+ GUI: move control wrapper classes to a separate set of files
[calf.git] / bigbull / conndiagram.py
blob3d2030f6227e6307f741c4a7b174043070c12115
1 import pygtk
2 pygtk.require('2.0')
3 import gtk
4 import cairo
5 import pangocairo
6 import pango
7 import goocanvas
8 import random
9 import math
11 def calc_extents(ctx, fontName, text):
12 layout = pangocairo.CairoContext(ctx).create_layout()
13 layout.set_font_description(pango.FontDescription(fontName))
14 layout.set_text(text)
15 return layout.get_pixel_size()
17 class Colors:
18 frame = 0x808080FF
19 text = 0xE0E0E0FF
20 box = 0x303030FF
21 defPort = 0x404040FF
22 audioPort = 0x004060FF
23 controlPort = 0x008000FF
24 eventPort = 0x800000FF
25 activePort = 0x808080FF
26 draggedWire = 0xFFFFFFFF
27 connectedWire = 0x808080FF
29 class VisibleWire():
30 def __init__(self, src, dest, wire):
31 """src is source ModulePort, dst is destination ModulePort, wire is a goocanvas.Path"""
32 self.src = src
33 self.dest = dest
34 self.wire = wire
35 def delete(self):
36 self.wire.remove()
37 self.src.module.remove_wire(self)
38 self.dest.module.remove_wire(self)
40 class ModulePort():
41 fontName = "DejaVu Sans 9"
42 type = "port"
43 def __init__(self, module, portData):
44 self.module = module
45 self.portData = portData
46 self.isInput = self.get_parser().is_port_input(portData)
47 self.box = self.title = None
49 def get_parser(self):
50 return self.module.get_parser()
52 def get_id(self):
53 return self.get_parser().get_port_id(self.portData)
55 def calc_width(self, ctx):
56 return calc_extents(ctx, self.fontName, self.get_parser().get_port_name(self.portData))[0] + 4 * self.module.margin
58 def render(self, ctx, parent, y):
59 module = self.module
60 (width, margin, spacing) = (module.width, module.margin, module.spacing)
61 al = "left"
62 portName = self.get_parser().get_port_name(self.portData)
63 title = goocanvas.Text(parent = parent, text = portName, font = self.fontName, width = width - 2 * margin, x = margin, y = y, alignment = al, fill_color_rgba = Colors.text, hint_metrics = cairo.HINT_METRICS_ON, pointer_events = False, wrap = False)
64 height = 1 + int(title.get_requested_height(ctx, width - 2 * margin))
65 title.ensure_updated()
66 bnds = title.get_bounds()
67 bw = bnds.x2 - bnds.x1 + 2 * margin
68 if not self.isInput:
69 title.translate(width - bw, 0)
70 color = self.get_parser().get_port_color(self.portData)
71 if self.isInput:
72 box = goocanvas.Rect(parent = parent, x = 0.5, y = y - 0.5, width = bw, height = height + 1, line_width = 0, fill_color_rgba = color, stroke_color_rgba = Colors.frame)
73 else:
74 box = goocanvas.Rect(parent = parent, x = width - bw - 0.5, y = y - 0.5, width = bw, height = height + 1, line_width = 0, fill_color_rgba = color, stroke_color_rgba = Colors.frame)
75 box.lower(title)
76 y += height + spacing
77 box.type = "port"
78 self.orig_color = color
79 box.object = box.module = self
80 box.portData = self.portData
81 title.portData = self.portData
82 self.box = box
83 self.title = title
84 return y
86 class ModuleBox():
87 margin = 2
88 spacing = 3
89 fontName = "DejaVu Sans Bold 9"
91 def __init__(self, parser, parent, moduleData, graph):
92 self.parser = parser
93 self.graph = graph
94 self.group = None
95 self.connect_candidate = None
96 self.parent = parent
97 self.moduleData = moduleData
98 self.group = goocanvas.Group(parent = self.parent)
99 self.wires = []
100 self.create_items()
102 def get_parser(self):
103 return self.parser
105 def create_items(self):
106 self.group.module = self
107 while self.group.get_n_children() > 0:
108 self.group.remove_child(0)
109 ctx = self.group.get_canvas().create_cairo_context()
110 self.title = self.get_parser().get_module_name(self.moduleData)
111 self.portDict = {}
112 width = self.get_title_width(ctx)
113 for (id, portData) in self.get_parser().get_module_port_list(self.moduleData):
114 mport = self.create_port(id, portData)
115 new_width = mport.calc_width(ctx)
116 if new_width > width:
117 width = new_width
118 self.width = width
119 y = self.render_title(ctx, 0)
120 for (id, portData) in self.get_parser().get_module_port_list(self.moduleData):
121 y = self.render_port(ctx, id, y)
122 self.rect = goocanvas.Rect(parent = self.group, width = self.width, height = y, line_width = 2, stroke_color_rgba = Colors.frame, fill_color_rgba = Colors.box, antialias = cairo.ANTIALIAS_GRAY)
123 self.rect.lower(self.titleItem)
124 self.rect.type = "module"
125 self.rect.object = self.rect.module = self
126 self.group.ensure_updated()
127 self.wire = None
129 def create_port(self, portId, portData):
130 mport = ModulePort(self, portData)
131 self.portDict[portId] = mport
132 return mport
134 def get_title_width(self, ctx):
135 return calc_extents(ctx, self.fontName, self.title)[0] + 4 * self.margin
137 def render_title(self, ctx, y):
138 self.titleItem = goocanvas.Text(parent = self.group, font = self.fontName, text = self.title, width = self.width, x = 0, y = y, alignment = "center", use_markup = True, fill_color_rgba = Colors.text, hint_metrics = cairo.HINT_METRICS_ON, antialias = cairo.ANTIALIAS_GRAY)
139 y += self.titleItem.get_requested_height(ctx, self.width) + self.spacing
140 return y
142 def render_port(self, ctx, portId, y):
143 mport = self.portDict[portId]
144 y = mport.render(ctx, self.group, y)
145 mport.box.connect_object("button-press-event", self.port_button_press, mport)
146 mport.title.connect_object("button-press-event", self.port_button_press, mport)
147 return y
149 def delete_items(self):
150 self.group.remove()
152 def port_button_press(self, mport, box, event):
153 if event.button == 1:
154 port_id = mport.get_id()
155 mport.box.props.fill_color_rgba = Colors.activePort
156 (x, y) = self.graph.port_endpoint(mport)
157 self.drag_wire = goocanvas.Path(parent = self.parent, stroke_color_rgba = Colors.draggedWire)
158 self.drag_wire.type = "tmp wire"
159 self.drag_wire.object = None
160 self.drag_wire.raise_(None)
161 self.graph.dragging = (self, port_id, self.drag_wire, x, y)
162 self.set_connect_candidate(None)
163 self.update_drag_wire(self.graph.dragging, x, y)
164 print "Port URI is " + port_id
165 return True
167 def dragging(self, tuple, x2, y2):
168 boundsGrp = self.group.get_bounds()
169 self.update_drag_wire(tuple, x2 + boundsGrp.x1, y2 + boundsGrp.y1)
171 def dragged(self, tuple, x2, y2):
172 # self.update_drag_wire(tuple, x2, y2)
173 wireitem = tuple[2]
174 port = self.portDict[tuple[1]]
175 self.graph.dragging = None
176 port.box.props.fill_color_rgba = port.orig_color
177 if self.connect_candidate != None:
178 # print "Connect: " + tuple[1] + " with " + self.connect_candidate.get_id()
179 try:
180 self.graph.connect(port, self.connect_candidate, wireitem)
181 except:
182 wireitem.remove()
183 raise
184 finally:
185 self.set_connect_candidate(None)
186 else:
187 wireitem.remove()
189 def set_connect_candidate(self, item):
190 if self.connect_candidate != item:
191 if self.connect_candidate != None:
192 self.connect_candidate.box.props.fill_color_rgba = self.connect_candidate.orig_color
193 self.connect_candidate = item
194 if item != None:
195 item.box.props.fill_color_rgba = Colors.activePort
197 def update_drag_wire(self, tuple, x2, y2):
198 (uri, x, y, dx, dy, wireitem) = (tuple[1], tuple[3], tuple[4], x2 - tuple[3], y2 - tuple[4], tuple[2])
199 wireitem.props.data = "M %0.0f,%0.0f C %0.0f,%0.0f %0.0f,%0.0f %0.0f,%0.0f" % (x, y, x+dx/2, y, x+dx/2, y+dy, x+dx, y+dy)
200 items = self.graph.get_data_items_at(x+dx, y+dy)
201 found = False
202 for type, obj, item in items:
203 if type == 'port':
204 if item.module != self and self.get_parser().can_connect(self.portDict[uri].portData, obj.portData):
205 found = True
206 self.set_connect_candidate(obj)
207 if not found and self.connect_candidate != None:
208 self.set_connect_candidate(None)
210 def update_wires(self):
211 for wire in self.wires:
212 self.graph.update_wire(wire)
214 def remove_wire(self, wire):
215 self.wires = [w for w in self.wires if w != wire]
217 class ConnectionGraphEditor:
218 def __init__(self, app, parser):
219 self.app = app
220 self.parser = parser
221 self.moving = None
222 self.dragging = None
223 self.modules = set()
224 pass
226 def get_parser(self):
227 return self.parser
229 def create(self):
230 self.create_canvas()
231 return self.canvas
233 def create_canvas(self):
234 self.canvas = goocanvas.Canvas()
235 self.canvas.props.automatic_bounds = True
236 self.canvas.set_size_request(640, 480)
237 self.canvas.set_scale(1)
238 #self.canvas.connect("size-allocate", self.update_canvas_bounds)
239 self.canvas.props.background_color_rgb = 0
240 self.canvas.props.integer_layout = False
241 self.canvas.update()
242 self.canvas.get_root_item().connect("motion-notify-event", self.canvas_motion_notify)
243 self.canvas.get_root_item().connect("button-release-event", self.canvas_button_release)
245 def get_items_at(self, x, y):
246 cr = self.canvas.create_cairo_context()
247 items = self.canvas.get_items_in_area(goocanvas.Bounds(x - 3, y - 3, x + 3, y + 3), True, True, False)
248 return items
250 def get_data_items_at(self, x, y):
251 items = self.get_items_at(x, y)
252 if items == None:
253 return []
254 data_items = []
255 for i in items:
256 if hasattr(i, 'type'):
257 data_items.append((i.type, i.object, i))
258 return data_items
260 def get_size(self):
261 bounds = self.canvas.get_bounds()
262 return (bounds[2] - bounds[0], bounds[3] - bounds[1])
264 def add_module_cb(self, params):
265 self.add_module(*params)
267 def add_module(self, moduleData, x, y):
268 mbox = ModuleBox(self.parser, self.canvas.get_root_item(), moduleData, self)
269 self.modules.add(mbox)
270 bounds = self.canvas.get_bounds()
271 if x == None:
272 (x, y) = (int(random.uniform(bounds[0], bounds[2] - 100)), int(random.uniform(bounds[1], bounds[3] - 50)))
273 mbox.group.translate(x, y)
274 mbox.group.connect("button-press-event", self.box_button_press)
275 mbox.group.connect("motion-notify-event", self.box_motion_notify)
276 mbox.group.connect("button-release-event", self.box_button_release)
277 return mbox
279 def delete_module(self, module):
280 self.modules.remove(mbox)
281 module.delete_items()
283 def get_port_map(self):
284 map = {}
285 for mod in self.modules:
286 map.update(mod.portDict)
287 return map
289 def canvas_motion_notify(self, group, widget, event):
290 if self.dragging != None:
291 self.dragging[0].dragging(self.dragging, event.x, event.y)
293 def canvas_button_release(self, group, widget, event):
294 if self.dragging != None and event.button == 1:
295 self.dragging[0].dragged(self.dragging, event.x, event.y)
297 def box_button_press(self, group, widget, event):
298 if event.button == 1:
299 group.raise_(None)
300 self.moving = group
301 self.motion_x = event.x
302 self.motion_y = event.y
303 return True
305 def box_button_release(self, group, widget, event):
306 if event.button == 1:
307 self.moving = None
309 def box_motion_notify(self, group, widget, event):
310 if self.moving == group:
311 self.moving.translate(event.x - self.motion_x, event.y - self.motion_y)
312 group.module.update_wires()
314 def port_endpoint(self, port):
315 bounds = port.box.get_bounds()
316 port = port.portData
317 if self.get_parser().is_port_input(port):
318 x = bounds.x1
319 else:
320 x = bounds.x2
321 y = (bounds.y1 + bounds.y2) / 2
322 return (x, y)
324 def update_wire(self, wire):
325 (x1, y1) = self.port_endpoint(wire.src)
326 (x2, y2) = self.port_endpoint(wire.dest)
327 xm = (x1 + x2) / 2
328 wire.wire.props.data = "M %0.0f,%0.0f C %0.0f,%0.0f %0.0f,%0.0f %0.0f,%0.0f" % (x1, y1, xm, y1, xm, y2, x2, y2)
330 def connect(self, p1, p2, wireitem = None):
331 # p1, p2 are ModulePort objects
332 # if wireitem is set, then this is manual connection, and parser.connect() is called
333 # if wireitem is None, then this is automatic connection, and parser.connect() is bypassed
334 if p2.isInput:
335 (src, dest) = (p1, p2)
336 else:
337 (dest, src) = (p1, p2)
338 if wireitem == None:
339 wireitem = goocanvas.Path(parent = self.canvas.get_root_item())
340 else:
341 self.get_parser().connect(src.portData, dest.portData)
342 wire = VisibleWire(src, dest, wireitem)
343 wireitem.type = "wire"
344 wireitem.object = wire
345 wireitem.props.stroke_color_rgba = Colors.connectedWire
346 src.module.wires.append(wire)
347 dest.module.wires.append(wire)
348 self.update_wire(wire)
350 def seg_distance(self, min1, max1, min2, max2):
351 if min1 > min2:
352 return self.seg_distance(min2, max2, min1, max1)
353 if min2 > max1 + 10:
354 return 0
355 return min2 - (max1 + 10)
357 # Squared radius of the circle containing the box
358 def box_radius2(self, box):
359 return (box.x2 - box.x1) ** 2 + (box.y2 - box.y1) ** 2
361 def repulsion(self, b1, b2):
362 minr2 = (self.box_radius2(b1) + self.box_radius2(b2))
363 b1x = (b1.x1 + b1.x2) / 2
364 b1y = (b1.y1 + b1.y2) / 2
365 b2x = (b2.x1 + b2.x2) / 2
366 b2y = (b2.y1 + b2.y2) / 2
367 r2 = (b2x - b1x) ** 2 + (b2y - b1y) ** 2
368 # Not repulsive ;)
369 #if r2 > minr2:
370 # return 0
371 return (b1x - b2x + 1j * (b1y - b2y)) * 2 * minr2/ (2 * r2**1.5)
373 def attraction(self, box, wire):
374 sign = +1
375 if box == wire.src.module:
376 sign = -1
377 b1 = wire.src.box.get_bounds();
378 b2 = wire.dest.box.get_bounds();
379 k = 0.003
380 if b1.x2 > b2.x1 - 40:
381 return k * 8 * sign * (b1.x2 - (b2.x1 - 40) + 1j * (b1.y1 - b2.y1))
382 return k * sign * (b1.x2 - b2.x1 + 1j * (b1.y1 - b2.y1))
384 def blow_up(self):
385 for m in self.modules:
386 m.velocity = 0+0j
387 m.bounds = m.group.get_bounds()
388 damping = 0.5
389 step = 2.0
390 cr = self.canvas.create_cairo_context()
391 w, h = self.canvas.allocation.width, self.canvas.allocation.height
392 temperature = 100
393 while True:
394 energy = 0.0
395 x = y = 0
396 for m1 in self.modules:
397 x += (m1.bounds.x1 + m1.bounds.x2) / 2
398 y += (m1.bounds.y1 + m1.bounds.y2) / 2
399 x /= len(self.modules)
400 y /= len(self.modules)
401 gforce = w / 2 - x + 1j * (h / 2 - y)
402 for m1 in self.modules:
403 force = gforce
404 force += temperature * random.random()
405 for m2 in self.modules:
406 if m1 == m2:
407 continue
408 force += self.repulsion(m1.bounds, m2.bounds)
409 for wi in m1.wires:
410 force += self.attraction(m1, wi)
411 if m1.bounds.x1 < 10: force -= m1.bounds.x1 - 10
412 if m1.bounds.y1 < 10: force -= m1.bounds.y1 * 1j - 10j
413 if m1.bounds.x2 > w: force -= (m1.bounds.x2 - w)
414 if m1.bounds.y2 > h: force -= (m1.bounds.y2 - h) * 1j
415 m1.velocity = (m1.velocity + force) * damping
416 energy += abs(m1.velocity) ** 2
417 for m1 in self.modules:
418 print "Velocity is (%s, %s)" % (m1.velocity.real, m1.velocity.imag)
419 m1.group.translate(step * m1.velocity.real, step * m1.velocity.imag)
420 m1.group.update(True, cr, m1.bounds)
421 m1.update_wires()
422 damping *= 0.99
423 temperature *= 0.99
424 self.canvas.draw(gtk.gdk.Rectangle(0, 0, w, h))
425 print "Energy is %s" % energy
426 if energy < 0.1:
427 break