GUI: Clean up creation of GUIs, hopefully fixing the crash-on-destroy this time.
[calf.git] / bigbull / conndiagram.py
blob9370487744c18fb294b98ca24131d3a076dba5dc
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 PortPalette(object):
18 def __init__(self, stroke, fill, activated_palette = None):
19 self.stroke = stroke
20 self.fill = fill
21 self.activated_palette = activated_palette
22 def get_stroke(self, port):
23 if self.activated_palette is not None and port.is_dragged():
24 return self.activated_palette.get_stroke(port)
25 return self.stroke
26 def get_fill(self, port):
27 if self.activated_palette is not None and port.is_dragged():
28 return self.activated_palette.get_fill(port)
29 return self.fill
31 class Colors:
32 frame = 0x4C4C4CFF
33 text = 0xE0E0E0FF
34 box = 0x242424FF
35 defPort = 0x404040FF
36 activePort = PortPalette(0xF0F0F0FF, 0x808080FF)
37 audioPort = PortPalette(0x204A87FF, 0x183868FF, activePort)
38 controlPort = PortPalette(0x008000FF, 0x00800080, activePort)
39 eventPort = PortPalette(0xA40000FF, 0x7C000080, activePort)
40 draggedWire = 0xFFFFFFFF
41 connectedWire = 0x808080FF
43 def path_move(x, y):
44 return "M %s %s " % (x, y)
46 def path_line(x, y):
47 return "L %s %s " % (x, y)
49 def path_curve(x1, y1, x2, y2, x3, y3):
50 return "C %0.0f,%0.0f %0.0f,%0.0f %0.0f,%0.0f" % (x1, y1, x2, y2, x3, y3)
52 def wireData(x1, y1, x2, y2):
53 dist = (x2 - x1) / 2
54 if dist < 30:
55 dist = 30 + (30 - dist) / 2
56 if dist > 60:
57 dist = 60
58 return path_move(x1, y1) + path_curve(x1 + dist, y1, x2 - dist, y2, x2, y2)
60 class VisibleWire():
61 def __init__(self, src, dest):
62 """src is source PortView, dst is destination PortView"""
63 self.src = src
64 self.dest = dest
65 self.mask = goocanvas.Path(parent = src.get_graph().get_root(), line_width=12, stroke_color_rgba = 0, pointer_events = goocanvas.EVENTS_ALL)
66 self.mask.type = "wire"
67 self.mask.object = self
68 self.wire = goocanvas.Path(parent = src.get_graph().get_root(), stroke_color_rgba = Colors.connectedWire, pointer_events = 0)
69 self.wire.type = "wirecore"
70 self.wire.object = self
71 self.update_shape()
73 def remove(self):
74 if self.wire is not None:
75 self.wire.remove()
76 self.mask.remove()
77 self.src.module.wires.remove(self)
78 self.dest.module.wires.remove(self)
79 self.wire = None
80 self.mask = None
82 def update_shape(self):
83 (x1, y1) = self.src.get_endpoint()
84 (x2, y2) = self.dest.get_endpoint()
85 data = wireData(x1, y1, x2, y2)
86 self.wire.props.data = data
87 self.mask.props.data = data
90 class Dragging():
91 def __init__(self, port_view, x, y):
92 self.module = port_view.module
93 self.port_view = port_view
94 self.x = x
95 self.y = y
96 self.drag_wire = goocanvas.Path(parent = self.get_graph().get_root(), stroke_color_rgba = Colors.draggedWire)
97 self.drag_wire.type = "tmp wire"
98 self.drag_wire.object = None
99 self.drag_wire.raise_(None)
100 self.connect_candidate = None
101 self.update_drag_wire(x, y)
103 def get_graph(self):
104 return self.module.graph
106 def update_shape(self, x2, y2):
107 if self.port_view.isInput:
108 self.drag_wire.props.data = wireData(x2, y2, self.x, self.y)
109 else:
110 self.drag_wire.props.data = wireData(self.x, self.y, x2, y2)
112 def dragging(self, x2, y2):
113 self.update_drag_wire(x2, y2)
115 def update_drag_wire(self, x2, y2):
116 items = self.get_graph().get_data_items_at(x2, y2)
117 found = None
118 for type, obj, item in items:
119 if type == 'port':
120 if item.module != self and self.get_graph().get_controller().can_connect(self.port_view.model, obj.model):
121 found = obj
122 self.set_connect_candidate(found)
123 if found is not None:
124 x2, y2 = found.get_endpoint()
125 self.update_shape(x2, y2)
127 def has_port_view(self, port_view):
128 if port_view == self.port_view:
129 return True
130 if port_view == self.connect_candidate:
131 return True
132 return False
134 def set_connect_candidate(self, item):
135 if self.connect_candidate != item:
136 old = self.connect_candidate
137 self.connect_candidate = item
138 if item is not None:
139 item.update_style()
140 if old is not None:
141 old.update_style()
143 def end_drag(self, x2, y2):
144 # self.update_drag_wire(tuple, x2, y2)
145 src, dst = self.port_view, self.connect_candidate
146 self.get_graph().dragging = None
147 src.update_style()
148 self.drag_wire.remove()
149 self.drag_wire = None
150 if dst is not None:
151 # print "Connect: " + tuple[1] + " with " + self.connect_candidate.get_id()
152 dst.update_style()
153 if src.isInput:
154 src, dst = dst, src
155 self.get_graph().get_controller().connect(src.model, dst.model)
157 class PortView():
158 fontName = "DejaVu Sans 11px"
159 type = "port"
160 def __init__(self, module, model):
161 self.module = module
162 self.model = model
163 self.isInput = model.is_port_input()
164 self.box = self.title = None
166 def get_graph(self):
167 return self.module.graph
169 def get_controller(self):
170 return self.module.get_controller()
172 def get_id(self):
173 return self.model.get_id()
175 def calc_width(self, ctx):
176 return calc_extents(ctx, self.fontName, self.model.get_name())[0] + 4 * self.module.margin + 15
178 @staticmethod
179 def input_arrow(x, y, w, h):
180 return path_move(x, y) + path_line(x + w - 10, y) + path_line(x + w, y + h / 2) + path_line(x + w - 10, y + h) + path_line(x, y + h) + path_line(x, y)
182 @staticmethod
183 def output_arrow(x, y, w, h):
184 return path_move(x + w, y) + path_line(x + 10, y) + path_line(x, y + h / 2) + path_line(x + 10, y + h) + path_line(x + w, y + h) + path_line(x + w, y)
186 def render(self, ctx, parent, y):
187 module = self.module
188 (width, margin, spacing) = (module.width, module.margin, module.spacing)
189 al = "left"
190 portName = self.model.get_name()
191 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)
192 height = 1 + int(title.get_requested_height(ctx, width - 2 * margin))
193 title.ensure_updated()
194 bnds = title.get_bounds()
195 bw = bnds.x2 - bnds.x1 + 2 * margin
196 if not self.isInput:
197 title.translate(width - bw - 2, 0)
198 else:
199 title.translate(2, 0)
200 bw += 10
201 if self.isInput:
202 box = goocanvas.Path(parent = parent, data = self.input_arrow(0.5, y - 0.5, bw, height + 1))
203 else:
204 box = goocanvas.Path(parent = parent, data = self.output_arrow(width - bw, y - 0.5, bw, height + 1))
205 box.lower(title)
206 y += height + spacing
207 box.type = "port"
208 box.object = box.module = self
209 box.model = self.model
210 title.model = self.model
211 self.box = box
212 self.title = title
213 self.update_style()
214 return y
216 def update_style(self):
217 color = self.model.get_port_color()
218 self.box.set_properties(fill_color_rgba = color.get_fill(self), stroke_color_rgba = color.get_stroke(self), line_width = 1, pointer_events = goocanvas.EVENTS_ALL)
220 def is_dragged(self):
221 dragging = self.get_graph().dragging
222 if dragging is not None and dragging.has_port_view(self):
223 return True
224 return False
226 def get_endpoint(self):
227 bounds = self.box.get_bounds()
228 if self.isInput:
229 x = bounds.x1
230 else:
231 x = bounds.x2
232 y = (bounds.y1 + bounds.y2) / 2
233 return (x, y)
235 def get_connections(self):
236 return [wire for wire in self.module.wires if wire.src == self or wire.dest == self]
238 class ModuleView():
239 margin = 2
240 spacing = 4
241 fontName = "DejaVu Sans Bold 9"
243 def __init__(self, controller, parent, model, graph):
244 self.controller = controller
245 self.graph = graph
246 self.group = None
247 self.connect_candidate = None
248 self.parent = parent
249 self.model = model
250 self.group = goocanvas.Group(parent = self.parent, pointer_events = goocanvas.EVENTS_ALL)
251 self.group.type = "module"
252 self.group.object = self.group.module = self
253 self.group.raise_(None)
254 self.rect = None
255 self.titleItem = None
256 self.ports = []
257 self.wires = []
258 self.refresh()
260 def get_controller(self):
261 return self.controller
263 def refresh(self):
264 self.title = self.model.get_name()
265 self.ports = []
266 self.portDict = {}
267 for model in self.model.get_port_list():
268 self.add_port(model)
269 self.render()
271 def remove(self):
272 while self.group.get_n_children() > 0:
273 self.group.remove_child(0)
274 self.rect = None
275 self.titleItem = None
277 def render(self):
278 self.remove()
279 ctx = self.group.get_canvas().create_cairo_context()
280 self.width = max([self.get_title_width(ctx)] + [port_view.calc_width(ctx) for port_view in self.ports])
281 y = self.render_title(ctx, 0.5)
282 for port_view in self.ports:
283 y = port_view.render(ctx, self.group, y)
284 self.rect = goocanvas.Rect(parent = self.group, x = 0.5, width = self.width, height = y)
285 self.update_style()
286 self.rect.lower(self.titleItem)
287 self.rect.type = "module"
288 self.rect.object = self.group.module = self
289 self.group.ensure_updated()
291 def add_port(self, model, pos = None):
292 if pos is None:
293 pos = len(self.ports)
294 view = PortView(self, model)
295 self.ports.insert(pos, view)
296 self.portDict[model.get_id()] = view
297 return view
299 def del_port(self, pos):
300 del self.portDict[self.ports[pos].model.get_id()]
301 self.ports.pop(pos)
303 def get_title_width(self, ctx):
304 return calc_extents(ctx, self.fontName, self.title)[0] + 4 * self.margin
306 def render_title(self, ctx, y):
307 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, pointer_events = goocanvas.EVENTS_NONE)
308 y += self.titleItem.get_requested_height(ctx, self.width) + 2 * self.spacing
309 return y
311 def render_port(self, ctx, port_view, y):
312 return port_view.render(ctx, self.group, y)
313 #port_view.box.connect_object("button-press-event", self.port_button_press, port_view)
314 #port_view.title.connect_object("button-press-event", self.port_button_press, port_view)
316 def delete_items(self):
317 self.group.remove()
318 for w in list(self.wires):
319 w.remove()
321 def update_wires(self):
322 for wire in self.wires:
323 wire.update_shape()
325 def translate(self, dx, dy):
326 self.group.translate(dx, dy)
327 self.group.ensure_updated()
328 self.update_wires()
330 def update_style(self):
331 self.rect.set_properties(line_width = 1, stroke_color_rgba = Colors.frame, fill_color_rgba = Colors.box, antialias = cairo.ANTIALIAS_GRAY, pointer_events = goocanvas.EVENTS_ALL)
333 class ConnectionGraphEditor:
334 def __init__(self, app, controller):
335 self.app = app
336 self.controller = controller
337 self.moving = None
338 self.dragging = None
339 self.modules = set()
340 pass
342 def get_controller(self):
343 return self.controller
345 def create(self, sx, sy):
346 self.create_canvas(sx, sy)
347 return self.canvas
349 def create_canvas(self, sx, sy):
350 self.canvas = goocanvas.Canvas()
351 self.canvas.props.automatic_bounds = True
352 self.canvas.set_size_request(sx, sy)
353 self.canvas.set_scale(1)
354 #self.canvas.connect("size-allocate", self.update_canvas_bounds)
355 self.canvas.props.background_color_rgb = 0
356 self.canvas.props.integer_layout = False
357 self.canvas.update()
358 self.canvas.connect("button-press-event", self.canvas_button_press_handler)
359 self.canvas.connect("motion-notify-event", self.canvas_motion_notify)
360 self.canvas.connect("button-release-event", self.canvas_button_release)
362 def get_canvas(self):
363 return self.canvas
365 def get_root(self):
366 return self.canvas.get_root_item()
368 def get_items_at(self, x, y):
369 return self.canvas.get_items_at(x, y, True)
371 def get_module_map(self, model):
372 map = {}
373 for view in self.modules:
374 map[view.model] = view
375 return map
377 def get_module_view(self, model):
378 for view in self.modules:
379 if view.model == model:
380 return view
381 return None
383 def get_port_map(self):
384 map = {}
385 for mod in self.modules:
386 map.update(mod.portDict)
387 return map
389 def get_port_view(self, port_model):
390 for mod in self.modules:
391 for p in mod.ports:
392 if p.model == port_model:
393 return p
394 return None
396 def get_data_items_at(self, x, y):
397 items = self.get_items_at(x, y)
398 if items == None:
399 return []
400 data_items = []
401 for i in items:
402 if hasattr(i, 'type'):
403 data_items.append((i.type, i.object, i))
404 return data_items
406 def get_size(self):
407 bounds = self.canvas.get_bounds()
408 return (bounds[2] - bounds[0], bounds[3] - bounds[1])
410 def add_module(self, model, x, y):
411 mbox = ModuleView(self.controller, self.canvas.get_root_item(), model, self)
412 self.modules.add(mbox)
413 bounds = self.canvas.get_bounds()
414 if x == None:
415 (x, y) = (int(random.uniform(bounds[0], bounds[2] - 100)), int(random.uniform(bounds[1], bounds[3] - 50)))
416 mbox.group.translate(x, y)
417 return mbox
419 def delete_module(self, module):
420 self.modules.remove(mbox)
421 module.delete_items()
423 def canvas_button_press_handler(self, widget, event):
424 if event.button == 1:
425 for itype, iobject, item in self.get_data_items_at(event.x, event.y):
426 if itype == 'module':
427 group = iobject.group
428 group.raise_(None)
429 for w in group.module.wires:
430 w.wire.raise_(None)
432 self.moving = group.module
433 self.motion_x = event.x
434 self.motion_y = event.y
435 return True
436 if itype == 'port':
437 port_view = iobject
438 module = port_view.module
439 (x, y) = port_view.get_endpoint()
440 self.dragging = Dragging(port_view, x, y)
441 port_view.update_style()
442 print "Port URI is " + port_view.get_id()
443 return True
444 elif event.button == 3:
445 self.app.canvas_popup_menu(event.x, event.y, event.time)
446 return True
448 def canvas_motion_notify(self, widget, event):
449 if self.dragging is not None:
450 self.dragging.dragging(event.x, event.y)
451 if self.moving is not None:
452 self.moving.translate(event.x - self.motion_x, event.y - self.motion_y)
453 self.moving.update_wires()
454 self.motion_x, self.motion_y = event.x, event.y
456 def canvas_button_release(self, widget, event):
457 if event.button == 1:
458 if self.moving is not None:
459 self.moving = None
460 if self.dragging is not None:
461 self.dragging.end_drag(event.x, event.y)
463 # Connect elements visually (but not logically, that's what controller is for)
464 def connect(self, src, dest):
465 wire = VisibleWire(src, dest)
466 src.module.wires.append(wire)
467 dest.module.wires.append(wire)
468 return wire
470 def disconnect(self, src, dest):
471 for w in src.module.wires:
472 if w.dest == dest:
473 w.remove()
474 return True
475 return False
477 def clear(self):
478 for m in self.modules:
479 m.delete_items()
480 self.modules.clear()
481 self.moving = None
482 self.dragging = None
484 def blow_up(self):
485 GraphDetonator().blow_up(self)
487 # I broke this at some point, and not in mood to fix it now
488 class GraphDetonator:
489 def seg_distance(self, min1, max1, min2, max2):
490 if min1 > min2:
491 return self.seg_distance(min2, max2, min1, max1)
492 if min2 > max1 + 10:
493 return 0
494 return min2 - (max1 + 10)
496 # Squared radius of the circle containing the box
497 def box_radius2(self, box):
498 return (box.x2 - box.x1) ** 2 + (box.y2 - box.y1) ** 2
500 def repulsion(self, b1, b2):
501 minr2 = (self.box_radius2(b1) + self.box_radius2(b2))
502 b1x = (b1.x1 + b1.x2) / 2
503 b1y = (b1.y1 + b1.y2) / 2
504 b2x = (b2.x1 + b2.x2) / 2
505 b2y = (b2.y1 + b2.y2) / 2
506 r2 = (b2x - b1x) ** 2 + (b2y - b1y) ** 2
507 # Not repulsive ;)
508 #if r2 > minr2:
509 # return 0
510 return (b1x - b2x + 1j * (b1y - b2y)) * 2 * minr2/ (2 * r2**1.5)
512 def attraction(self, box, wire):
513 sign = +1
514 if box == wire.src.module:
515 sign = -1
516 b1 = wire.src.box.get_bounds();
517 b2 = wire.dest.box.get_bounds();
518 k = 0.003
519 if b1.x2 > b2.x1 - 40:
520 return k * 8 * sign * (b1.x2 - (b2.x1 - 40) + 1j * (b1.y1 - b2.y1))
521 return k * sign * (b1.x2 - b2.x1 + 1j * (b1.y1 - b2.y1))
523 def blow_up(self, graph):
524 modules = graph.modules
525 canvas = graph.canvas
526 for m in modules:
527 m.velocity = 0+0j
528 m.bounds = m.group.get_bounds()
529 damping = 0.5
530 step = 2.0
531 cr = canvas.create_cairo_context()
532 w, h = canvas.allocation.width, graph.canvas.allocation.height
533 temperature = 100
534 while True:
535 energy = 0.0
536 x = y = 0
537 for m1 in modules:
538 x += (m1.bounds.x1 + m1.bounds.x2) / 2
539 y += (m1.bounds.y1 + m1.bounds.y2) / 2
540 x /= len(modules)
541 y /= len(modules)
542 gforce = w / 2 - x + 1j * (h / 2 - y)
543 for m1 in modules:
544 force = gforce
545 force += temperature * random.random()
546 for m2 in modules:
547 if m1 == m2:
548 continue
549 force += self.repulsion(m1.bounds, m2.bounds)
550 for wi in m1.wires:
551 force += self.attraction(m1, wi)
552 if m1.bounds.x1 < 10: force -= m1.bounds.x1 - 10
553 if m1.bounds.y1 < 10: force -= m1.bounds.y1 * 1j - 10j
554 if m1.bounds.x2 > w: force -= (m1.bounds.x2 - w)
555 if m1.bounds.y2 > h: force -= (m1.bounds.y2 - h) * 1j
556 m1.velocity = (m1.velocity + force) * damping
557 energy += abs(m1.velocity) ** 2
558 for m1 in modules:
559 print "Velocity is (%s, %s)" % (m1.velocity.real, m1.velocity.imag)
560 m1.group.translate(step * m1.velocity.real, step * m1.velocity.imag)
561 m1.group.ensure_updated()
562 #m1.group.update(True, cr, m1.bounds)
563 m1.update_wires()
564 damping *= 0.99
565 temperature *= 0.99
566 canvas.draw(gtk.gdk.Rectangle(0, 0, w, h))
567 print "Energy is %s" % energy
568 if energy < 0.1:
569 break