--- /dev/null
+Copyright (c) 2018 Javier Sancho
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
--- /dev/null
+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.stacklayout import StackLayout
+
+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]
+
+ 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 type(widget) is 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)
+ elif widget.point_area == 'left':
+ parent = widget.parent
+ box = parent.create_box()
+ parent.add_widget(box, index=idx)
+ box.add_widget(self)
+ parent.remove_widget(widget)
+ box.add_widget(widget)
+ else:
+ parent = widget.parent
+ box = parent.create_box()
+ parent.add_widget(box, index=idx)
+ parent.remove_widget(widget)
+ box.add_widget(widget)
+ box.add_widget(self)
+ widget.point()
+ break
+ else:
+ self.form_canvas.add_widget(self)
+
+ def on_touch_down(self, touch):
+ if self.collide_point(*touch.pos):
+ if touch.grab_current is None:
+ touch.grab(self)
+ 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()
+ 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)
+
+
+class FormCanvas(ButtonBehavior, StackLayout):
+ def __init__(self, *args, **kwargs):
+ super(FormCanvas, self).__init__(*args, **kwargs)
+ self.n = 0
+ self.orientation = 'lr-tb'
+ self.padding = [10, 10, 10, 10]
+ self.spacing = [10, 10]
+
+ self.widgets_height = 40
+ self.widgets_size_hint = (1, None)
+
+ def on_press(self, *args):
+ self.n += 1
+ g = Grabbable(
+ height=self.widgets_height,
+ size_hint=self.widgets_size_hint
+ )
+ g.add_widget(Button(text=str(self.n)))
+ self.add_widget(g)
+ #print(self.export_to_kv(self))
+
+ def create_box(self):
+ return BoxLayout(
+ orientation='horizontal',
+ height=self.widgets_height,
+ size_hint=self.widgets_size_hint,
+ spacing=self.spacing[0]
+ )
+
+ def export_to_kv(self, widget, indent=''):
+ kv = """StackLayout:
+ orientation: '{orientation}'
+ padding: {padding}
+ spacing: {spacing}
+""".format(orientation=self.orientation, padding=self.padding, spacing=self.spacing)
+
+ indent = ' '
+ stack = [self]
+
+ widgets = self.walk(restrict=True)
+ next(widgets) # the first widget is the FormCanvas
+ for widget in widgets:
+ if not type(widget) is Grabbable:
+ # Look for the widget position inside the tree
+ parent = widget.parent
+ if type(parent) is Grabbable:
+ parent = parent.parent
+ while not parent is stack[-1]:
+ stack.pop()
+
+ # Widget header
+ kv += '{indent}{widget}:\n'.format(indent=indent*len(stack), widget=type(widget).__name__)
+
+ stack.append(widget)
+
+ # Widget attributes
+ kv += """{indent}height: 40
+{indent}size_hint: (1., None)
+{indent}text: '{text}'
+""".format(indent=indent*len(stack), text=widget.text)
+
+ return kv