Improve exported Kv
[kivyforms.git] / kivyforms / formcanvas.py
1 from kivy.graphics import Color, Line
2 from kivy.uix.boxlayout import BoxLayout
3 from kivy.uix.button import Button
4 from kivy.uix.label import Label
5 from kivy.uix.stacklayout import StackLayout
6
7
8 """
9 Some common methods
10 """
11 def get_form_canvas(widget):
12     parent = widget.parent
13     while parent and not isinstance(parent, FormCanvas):
14         if parent is parent.parent:
15             raise Exception("Infinite loop when searching FormCanvas")
16         parent = parent.parent
17     return parent
18
19 def get_index(widget):
20     for idx in range(len(widget.parent.children)):
21         if widget.parent.children[idx] is widget:
22             return idx
23     return None
24
25 def get_zone(widget, x, y):
26     side = widget.x + widget.width / 4
27     if x > side * 3:
28         return 'right'
29     elif x < side:
30         return 'left'
31     elif y > widget.y + widget.height / 2:
32         return 'top'
33     else:
34         return 'bottom'
35
36
37 class Destination(Label):
38     def __init__(self, **kwargs):
39         super(Destination, self).__init__(**kwargs)
40         self.text = 'Widget goes here'
41         self.color = [1, 1, 0, 1]
42         with self.canvas.after:
43             Color(1, 1, 0, 1)
44             self.box = Line(dash_length=8, dash_offset=4)
45
46     def update_box(self):
47         self.box.points = [
48             self.x, self.y,
49             self.x, self.y + self.height,
50             self.x + self.width, self.y + self.height,
51             self.x + self.width, self.y,
52             self.x, self.y
53         ]
54
55     def on_pos(self, instance, value):
56         self.update_box()
57
58     def on_size(self, instance, value):
59         self.update_box()
60
61
62 class Grabbable(BoxLayout):
63     def __init__(self, **kwargs):
64         super(Grabbable, self).__init__(**kwargs)
65         self.detached = False
66
67     def point(self, touch):
68         get_form_canvas(self).point_widget(self, touch.pos)
69
70     def detach(self):
71         self.detached = True
72         get_form_canvas(self).detach_widget(self)
73
74     def attach(self, touch):
75         if self.detached:
76             form_canvas = None
77             for widget in self.get_root_window().children[1].walk():
78                 if isinstance(widget, FormCanvas) and widget.collide_point(*touch.pos):
79                     form_canvas = widget
80                     break
81             else:
82                 raise Exception("FormCanvas not found")
83             form_canvas.attach_widget(self)
84             self.detached = False
85
86     def move(self, x, y):
87         self.pos = [x - self.width / 2, y - self.height / 2]
88         
89     def on_touch_down(self, touch):
90         if self.collide_point(*touch.pos):
91             if touch.grab_current is None:
92                 touch.grab(self)
93                 self.detach()
94                 self.move(*touch.pos)
95                 return True
96         return super(Grabbable, self).on_touch_down(touch)
97
98     def on_touch_move(self, touch):
99         if touch.grab_current is self:
100             self.move(*touch.pos)
101         else:
102             if self.parent and self.parent != self.get_root_window():
103                 if self.collide_point(touch.x, touch.y):
104                     self.point(touch)
105
106     def on_touch_up(self, touch):
107         if touch.grab_current is self:
108             touch.ungrab(self)
109             self.attach(touch)
110             
111
112 class FormCanvas(BoxLayout):
113     def __init__(self, *args, **kwargs):
114         super(FormCanvas, self).__init__(*args, **kwargs)
115
116         self._canvas = StackLayout(
117             orientation='lr-tb',
118             padding=[10, 10, 10, 10],
119             spacing=[10, 10]
120         )
121         super(FormCanvas, self).add_widget(self._canvas)
122
123         self.widgets_height = 40
124         self.widgets_size_hint = (1, None)
125
126     def add_widget(self, widget):
127         g = Grabbable(
128             height=self.widgets_height,
129             size_hint=self.widgets_size_hint
130         )
131         widget.height = self.widgets_height
132         widget.size_hint = self.widgets_size_hint
133         g.add_widget(widget)
134         self._canvas.add_widget(g)
135
136     def detach_widget(self, widget):
137         "Detach grabbable widget from canvas and show widget destination"
138
139         # Show widget destination
140         self.destination = Destination()
141         self.destination.height = self.widgets_height
142         self.destination.size_hint = self.widgets_size_hint
143         widget.parent.add_widget(self.destination, index=get_index(widget))
144
145         # Place widget in the root window
146         widget.parent.remove_widget(widget)
147         widget.size_hint = (None, None)
148         widget.width = 150
149         widget.height = 50
150         self.get_root_window().add_widget(widget)
151
152     def point_widget(self, widget, position):
153         if not self.destination:
154             raise Exception("Wrong status: destination point not specified")
155
156         # Widget info about destination
157         widget_idx = get_index(widget)
158         zone = get_zone(widget, *position)
159
160         # Remove destination
161         box = self.destination.parent
162         box.remove_widget(self.destination)
163
164         # Put destination in the right place
165         if zone in ('left', 'right') and widget.parent.orientation != 'horizontal':
166             parent = widget.parent
167             box = self.create_box()
168             parent.add_widget(box, index=widget_idx)
169             parent.remove_widget(widget)
170             box.add_widget(widget)
171             if zone == 'left':
172                 box.add_widget(self.destination, index=1)
173             else:
174                 box.add_widget(self.destination, index=0)
175         else:
176             widget.parent.add_widget(self.destination, index=widget_idx)
177
178         # Remove useless boxes
179         if isinstance(box, BoxLayout) and len(box.children) == 1:
180             child = box.children[0]
181             box.remove_widget(child)
182             box.parent.add_widget(child, index=get_index(box))
183             box.parent.remove_widget(box)
184
185     def attach_widget(self, widget):
186         """
187         Attach grabbable widget to the destination
188         """
189         widget.parent.remove_widget(widget)
190         widget.height = self.widgets_height
191         widget.size_hint = self.widgets_size_hint
192
193         dest_idx = get_index(self.destination)
194         self.destination.parent.add_widget(widget, index=dest_idx)
195         self.destination.parent.remove_widget(self.destination)
196         self.destination = None
197
198     def create_box(self):
199         return BoxLayout(
200             orientation='horizontal',
201             height=self.widgets_height,
202             size_hint=self.widgets_size_hint,
203             spacing=self._canvas.spacing[0]
204         )
205
206     def export_to_kv(self):
207         kv = """StackLayout:
208     orientation: '{orientation}'
209     padding: {padding}
210     spacing: {spacing}
211 """.format(orientation=self._canvas.orientation, padding=self._canvas.padding, spacing=self._canvas.spacing)
212
213         indent = '    '
214         stack = [self._canvas]
215
216         widgets = self.walk(restrict=True)
217         next(widgets)    # the first widget is the FormCanvas
218         next(widgets)    # and the second is the inner StackLayout
219         for widget in widgets:
220             if not isinstance(widget, Grabbable):
221                 # Look for the widget position inside the tree
222                 parent = widget.parent
223                 if isinstance(parent, Grabbable):
224                     parent = parent.parent
225                 while not parent is stack[-1]:
226                     stack.pop()
227
228                 # Widget header
229                 kv += '{indent}{widget}:\n'.format(indent=indent*len(stack), widget=type(widget).__name__)
230
231                 stack.append(widget)
232
233                 # Widget attributes
234                 for attr in ('height', 'size_hint', 'text', 'spacing'):
235                     if hasattr(widget, attr):
236                         value = getattr(widget, attr)
237                         if type(value) is str:
238                             value = "'" + value + "'"
239                         kv += "{indent}{attr}: {value}\n".format(
240                             indent=indent*len(stack),
241                             attr=attr,
242                             value=value
243                         )
244
245         return kv