11 def calc_extents(ctx
, fontName
, text
):
12 layout
= pangocairo
.CairoContext(ctx
).create_layout()
13 layout
.set_font_description(pango
.FontDescription(fontName
))
15 return layout
.get_pixel_size()
17 class PortPalette(object):
18 def __init__(self
, stroke
, fill
, activated_palette
= None):
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
)
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
)
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
44 return "M %s %s " % (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
):
55 dist
= 30 + (30 - dist
) / 2
58 return path_move(x1
, y1
) + path_curve(x1
+ dist
, y1
, x2
- dist
, y2
, x2
, y2
)
61 def __init__(self
, src
, dest
):
62 """src is source PortView, dst is destination PortView"""
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
74 if self
.wire
is not None:
77 self
.src
.module
.wires
.remove(self
)
78 self
.dest
.module
.wires
.remove(self
)
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
91 def __init__(self
, port_view
, x
, y
):
92 self
.module
= port_view
.module
93 self
.port_view
= port_view
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
)
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
)
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
)
118 for type, obj
, item
in items
:
120 if item
.module
!= self
and self
.get_graph().get_controller().can_connect(self
.port_view
.model
, obj
.model
):
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
:
130 if port_view
== self
.connect_candidate
:
134 def set_connect_candidate(self
, item
):
135 if self
.connect_candidate
!= item
:
136 old
= self
.connect_candidate
137 self
.connect_candidate
= item
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
148 self
.drag_wire
.remove()
149 self
.drag_wire
= None
151 # print "Connect: " + tuple[1] + " with " + self.connect_candidate.get_id()
155 self
.get_graph().get_controller().connect(src
.model
, dst
.model
)
158 fontName
= "DejaVu Sans 11px"
160 def __init__(self
, module
, model
):
163 self
.isInput
= model
.is_port_input()
164 self
.box
= self
.title
= None
167 return self
.module
.graph
169 def get_controller(self
):
170 return self
.module
.get_controller()
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
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
)
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
):
188 (width
, margin
, spacing
) = (module
.width
, module
.margin
, module
.spacing
)
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
197 title
.translate(width
- bw
- 2, 0)
199 title
.translate(2, 0)
202 box
= goocanvas
.Path(parent
= parent
, data
= self
.input_arrow(0.5, y
- 0.5, bw
, height
+ 1))
204 box
= goocanvas
.Path(parent
= parent
, data
= self
.output_arrow(width
- bw
, y
- 0.5, bw
, height
+ 1))
206 y
+= height
+ spacing
208 box
.object = box
.module
= self
209 box
.model
= self
.model
210 title
.model
= self
.model
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
):
226 def get_endpoint(self
):
227 bounds
= self
.box
.get_bounds()
232 y
= (bounds
.y1
+ bounds
.y2
) / 2
235 def get_connections(self
):
236 return [wire
for wire
in self
.module
.wires
if wire
.src
== self
or wire
.dest
== self
]
241 fontName
= "DejaVu Sans Bold 9"
243 def __init__(self
, controller
, parent
, model
, graph
):
244 self
.controller
= controller
247 self
.connect_candidate
= None
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)
255 self
.titleItem
= None
260 def get_controller(self
):
261 return self
.controller
264 self
.title
= self
.model
.get_name()
267 for model
in self
.model
.get_port_list():
272 while self
.group
.get_n_children() > 0:
273 self
.group
.remove_child(0)
275 self
.titleItem
= None
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
)
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):
293 pos
= len(self
.ports
)
294 view
= PortView(self
, model
)
295 self
.ports
.insert(pos
, view
)
296 self
.portDict
[model
.get_id()] = view
299 def del_port(self
, pos
):
300 del self
.portDict
[self
.ports
[pos
].model
.get_id()]
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
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
):
318 for w
in list(self
.wires
):
321 def update_wires(self
):
322 for wire
in self
.wires
:
325 def translate(self
, dx
, dy
):
326 self
.group
.translate(dx
, dy
)
327 self
.group
.ensure_updated()
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
):
336 self
.controller
= controller
342 def get_controller(self
):
343 return self
.controller
345 def create(self
, sx
, sy
):
346 self
.create_canvas(sx
, sy
)
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
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
):
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
):
373 for view
in self
.modules
:
374 map[view
.model
] = view
377 def get_module_view(self
, model
):
378 for view
in self
.modules
:
379 if view
.model
== model
:
383 def get_port_map(self
):
385 for mod
in self
.modules
:
386 map.update(mod
.portDict
)
389 def get_port_view(self
, port_model
):
390 for mod
in self
.modules
:
392 if p
.model
== port_model
:
396 def get_data_items_at(self
, x
, y
):
397 items
= self
.get_items_at(x
, y
)
402 if hasattr(i
, 'type'):
403 data_items
.append((i
.type, i
.object, i
))
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()
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
)
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
429 for w
in group
.module
.wires
:
432 self
.moving
= group
.module
433 self
.motion_x
= event
.x
434 self
.motion_y
= event
.y
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()
444 elif event
.button
== 3:
445 self
.app
.canvas_popup_menu(event
.x
, event
.y
, event
.time
)
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:
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
)
470 def disconnect(self
, src
, dest
):
471 for w
in src
.module
.wires
:
478 for m
in self
.modules
:
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
):
491 return self
.seg_distance(min2
, max2
, min1
, max1
)
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
510 return (b1x
- b2x
+ 1j
* (b1y
- b2y
)) * 2 * minr2
/ (2 * r2
**1.5)
512 def attraction(self
, box
, wire
):
514 if box
== wire
.src
.module
:
516 b1
= wire
.src
.box
.get_bounds();
517 b2
= wire
.dest
.box
.get_bounds();
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
528 m
.bounds
= m
.group
.get_bounds()
531 cr
= canvas
.create_cairo_context()
532 w
, h
= canvas
.allocation
.width
, graph
.canvas
.allocation
.height
538 x
+= (m1
.bounds
.x1
+ m1
.bounds
.x2
) / 2
539 y
+= (m1
.bounds
.y1
+ m1
.bounds
.y2
) / 2
542 gforce
= w
/ 2 - x
+ 1j
* (h
/ 2 - y
)
545 force
+= temperature
* random
.random()
549 force
+= self
.repulsion(m1
.bounds
, m2
.bounds
)
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
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)
566 canvas
.draw(gtk
.gdk
.Rectangle(0, 0, w
, h
))
567 print "Energy is %s" % energy