Reserve space for widget when it's moving
authorJavier Sancho <jsf@jsancho.org>
Tue, 24 Jul 2018 14:53:57 +0000 (16:53 +0200)
committerJavier Sancho <jsf@jsancho.org>
Tue, 24 Jul 2018 14:53:57 +0000 (16:53 +0200)
kivyforms/formcanvas.py

index 0e233fb..8013ef6 100644 (file)
@@ -2,117 +2,112 @@ from kivy.graphics import Color, Line
 from kivy.uix.behaviors import ButtonBehavior
 from kivy.uix.boxlayout import BoxLayout
 from kivy.uix.button import Button
+from kivy.uix.label import Label
 from kivy.uix.stacklayout import StackLayout
 
+
+"""
+Some common methods
+"""
+def get_form_canvas(widget):
+    parent = widget.parent
+    while parent and not isinstance(parent, FormCanvas):
+        if parent is parent.parent:
+            raise Exception("Infinite loop when searching FormCanvas")
+        parent = parent.parent
+    return parent
+
+def get_index(widget):
+    for idx in range(len(widget.parent.children)):
+        if widget.parent.children[idx] is widget:
+            return idx
+    return None
+
+def get_zone(widget, x, y):
+    side = widget.x + widget.width / 4
+    if x > side * 3:
+        return 'right'
+    elif x < side:
+        return 'left'
+    elif y > widget.y + widget.height / 2:
+        return 'top'
+    else:
+        return 'bottom'
+
+
+class Destination(Label):
+    def __init__(self, **kwargs):
+        super(Destination, self).__init__(**kwargs)
+        self.text = 'Widget goes here'
+        self.color = [1, 1, 0, 1]
+        with self.canvas.after:
+            Color(1, 1, 0, 1)
+            self.box = Line(dash_length=8, dash_offset=4)
+
+    def update_box(self):
+        self.box.points = [
+            self.x, self.y,
+            self.x, self.y + self.height,
+            self.x + self.width, self.y + self.height,
+            self.x + self.width, self.y,
+            self.x, self.y
+        ]
+
+    def on_pos(self, instance, value):
+        self.update_box()
+
+    def on_size(self, instance, value):
+        self.update_box()
+
+
 class Grabbable(BoxLayout):
     def __init__(self, **kwargs):
         super(Grabbable, self).__init__(**kwargs)
         self.detached = False
-        self.form_canvas = None
 
-        self.point_area = None
-        with self.canvas.after:
-            Color(1, 1, 0)
-            self.line = Line(points=[])
-
-    def point(self, touch=None):
-        if touch:
-            side = self.x + self.width / 6
-            if touch.x > side * 5:
-                self.point_area = 'right'
-                self.line.points = [self.x + self.width + 5, self.y + 5, self.x + self.width + 5, self.y + self.height - 5]
-            elif touch.x < side:
-                self.point_area = 'left'
-                self.line.points = [self.x - 5, self.y + 5, self.x - 5, self.y + self.height - 5]
-            elif touch.y > self.y + self.height / 2:
-                self.point_area = 'top'
-                self.line.points = [self.x + 5, self.y + self.height + 5, self.x + self.width - 5, self.y + self.height + 5]
-            else:
-                self.point_area = 'bottom'
-                self.line.points = [self.x + 5, self.y - 5, self.x + self.width - 5, self.y - 5]
+    def point(self, touch):
+        get_form_canvas(self).point_widget(self, touch.pos)
 
-        else:
-            self.point_area = None
-            self.line.points = []
-
-    def get_form_canvas(self):
-        parent = self.parent
-        while parent and not isinstance(parent, FormCanvas):
-            parent = parent.parent
-        return parent
-
-    def get_index(self):
-        for idx in range(len(self.parent.children)):
-            if self.parent.children[idx] is self:
-                return idx
-        return None
-            
     def detach(self):
         self.detached = True
-        self.form_canvas = self.get_form_canvas()
-        root_window = self.get_root_window()
-        self.parent.remove_widget(self)
-        self.size_hint = (None, None)
-        self.width = 150
-        self.height = 50
-        root_window.add_widget(self)
-
-    def attach(self, x, y):
-        self.detached = False
-        self.parent.remove_widget(self)
-        self.height = self.form_canvas.widgets_height
-        self.size_hint = self.form_canvas.widgets_size_hint
-
-        for widget in self.form_canvas.walk(restrict=True):
-            if isinstance(widget, Grabbable) and widget.collide_point(x, y):
-                idx = widget.get_index()
-                if widget.point_area == 'top':
-                    widget.parent.add_widget(self, index=idx + 1)
-                elif widget.point_area == 'bottom':
-                    widget.parent.add_widget(self, index=idx)
-                else:
-                    parent = widget.parent
-                    if not isinstance(parent, BoxLayout):
-                        box = parent.create_box()
-                        parent.add_widget(box, index=idx)
-                        parent.remove_widget(widget)
-                        box.add_widget(widget)
-                        idx = 0
-                        parent = box
-                    if widget.point_area == 'left':
-                        parent.add_widget(self, index=idx + 1)
-                    else:
-                        parent.add_widget(self, index=idx)
-                widget.point()
-                break
-        else:
-            self.form_canvas.add_widget(self)
+        get_form_canvas(self).detach_widget(self)
+
+    def attach(self, touch):
+        if self.detached:
+            form_canvas = None
+            for widget in self.get_root_window().children[1].walk():
+                if isinstance(widget, FormCanvas) and widget.collide_point(*touch.pos):
+                    form_canvas = widget
+                    break
+            else:
+                raise Exception("FormCanvas not found")
+            form_canvas.attach_widget(self)
+            self.detached = False
+
+    def move(self, x, y):
+        self.pos = [x - self.width / 2, y - self.height / 2]
         
     def on_touch_down(self, touch):
         if self.collide_point(*touch.pos):
             if touch.grab_current is None:
                 touch.grab(self)
+                self.detach()
+                self.move(*touch.pos)
                 return True
         return super(Grabbable, self).on_touch_down(touch)
 
     def on_touch_move(self, touch):
         if touch.grab_current is self:
-            self.pos = [touch.x - self.width / 2, touch.y - self.height / 2]
-            self.point()
-            if not self.detached:
-                self.detach()
+            self.move(*touch.pos)
         else:
             if self.parent and self.parent != self.get_root_window():
                 if self.collide_point(touch.x, touch.y):
                     self.point(touch)
-                else:
-                    self.point()
 
     def on_touch_up(self, touch):
         if touch.grab_current is self:
             touch.ungrab(self)
-            if self.detached:
-                self.attach(touch.x, touch.y)
+            self.attach(touch)
             
 
 class FormCanvas(ButtonBehavior, StackLayout):
@@ -136,6 +131,68 @@ class FormCanvas(ButtonBehavior, StackLayout):
         self.add_widget(g)
         #print(self.export_to_kv(self))
 
+    def detach_widget(self, widget):
+        "Detach grabbable widget from canvas and show widget destination"
+
+        # Show widget destination
+        self.destination = Destination()
+        self.destination.height = self.widgets_height
+        self.destination.size_hint = self.widgets_size_hint
+        widget.parent.add_widget(self.destination, index=get_index(widget))
+
+        # Place widget in the root window
+        widget.parent.remove_widget(widget)
+        widget.size_hint = (None, None)
+        widget.width = 150
+        widget.height = 50
+        self.get_root_window().add_widget(widget)
+
+    def point_widget(self, widget, position):
+        if not self.destination:
+            raise Exception("Wrong status: destination point not specified")
+
+        # Widget info about destination
+        widget_idx = get_index(widget)
+        zone = get_zone(widget, *position)
+
+        # Remove destination
+        box = self.destination.parent
+        box.remove_widget(self.destination)
+
+        # Put destination in the right place
+        if zone in ('left', 'right') and widget.parent.orientation != 'horizontal':
+            parent = widget.parent
+            box = parent.create_box()
+            parent.add_widget(box, index=widget_idx)
+            parent.remove_widget(widget)
+            box.add_widget(widget)
+            if zone == 'left':
+                box.add_widget(self.destination, index=1)
+            else:
+                box.add_widget(self.destination, index=0)
+        else:
+            widget.parent.add_widget(self.destination, index=widget_idx)
+
+        # Remove useless boxes
+        if isinstance(box, BoxLayout) and len(box.children) == 1:
+            child = box.children[0]
+            box.remove_widget(child)
+            box.parent.add_widget(child, index=get_index(box))
+            box.parent.remove_widget(box)
+
+    def attach_widget(self, widget):
+        """
+        Attach grabbable widget to the destination
+        """
+        widget.parent.remove_widget(widget)
+        widget.height = self.widgets_height
+        widget.size_hint = self.widgets_size_hint
+
+        dest_idx = get_index(self.destination)
+        self.destination.parent.add_widget(widget, index=dest_idx)
+        self.destination.parent.remove_widget(self.destination)
+        self.destination = None
+
     def create_box(self):
         return BoxLayout(
             orientation='horizontal',