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()
22 audioPort
= 0x004060FF
23 controlPort
= 0x008000FF
24 eventPort
= 0x800000FF
25 activePort
= 0x808080FF
26 draggedWire
= 0xFFFFFFFF
27 connectedWire
= 0x808080FF
30 def __init__(self
, src
, dest
, wire
):
31 """src is source ModulePort, dst is destination ModulePort, wire is a goocanvas.Path"""
37 self
.src
.module
.remove_wire(self
)
38 self
.dest
.module
.remove_wire(self
)
41 fontName
= "DejaVu Sans 9"
43 def __init__(self
, module
, portData
):
45 self
.portData
= portData
46 self
.isInput
= self
.get_parser().is_port_input(portData
)
47 self
.box
= self
.title
= None
50 return self
.module
.get_parser()
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
):
60 (width
, margin
, spacing
) = (module
.width
, module
.margin
, module
.spacing
)
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
69 title
.translate(width
- bw
, 0)
70 color
= self
.get_parser().get_port_color(self
.portData
)
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
)
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
)
78 self
.orig_color
= color
79 box
.object = box
.module
= self
80 box
.portData
= self
.portData
81 title
.portData
= self
.portData
89 fontName
= "DejaVu Sans Bold 9"
91 def __init__(self
, parser
, parent
, moduleData
, graph
):
95 self
.connect_candidate
= None
97 self
.moduleData
= moduleData
98 self
.group
= goocanvas
.Group(parent
= self
.parent
)
102 def get_parser(self
):
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
)
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
:
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()
129 def create_port(self
, portId
, portData
):
130 mport
= ModulePort(self
, portData
)
131 self
.portDict
[portId
] = 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
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
)
149 def delete_items(self
):
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
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)
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()
180 self
.graph
.connect(port
, self
.connect_candidate
, wireitem
)
185 self
.set_connect_candidate(None)
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
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
)
202 for type, obj
, item
in items
:
204 if item
.module
!= self
and self
.get_parser().can_connect(self
.portDict
[uri
].portData
, obj
.portData
):
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
):
226 def get_parser(self
):
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
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)
250 def get_data_items_at(self
, x
, y
):
251 items
= self
.get_items_at(x
, y
)
256 if hasattr(i
, 'type'):
257 data_items
.append((i
.type, i
.object, i
))
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()
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
)
279 def delete_module(self
, module
):
280 self
.modules
.remove(mbox
)
281 module
.delete_items()
283 def get_port_map(self
):
285 for mod
in self
.modules
:
286 map.update(mod
.portDict
)
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:
301 self
.motion_x
= event
.x
302 self
.motion_y
= event
.y
305 def box_button_release(self
, group
, widget
, event
):
306 if event
.button
== 1:
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()
317 if self
.get_parser().is_port_input(port
):
321 y
= (bounds
.y1
+ bounds
.y2
) / 2
324 def update_wire(self
, wire
):
325 (x1
, y1
) = self
.port_endpoint(wire
.src
)
326 (x2
, y2
) = self
.port_endpoint(wire
.dest
)
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
335 (src
, dest
) = (p1
, p2
)
337 (dest
, src
) = (p1
, p2
)
339 wireitem
= goocanvas
.Path(parent
= self
.canvas
.get_root_item())
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
):
352 return self
.seg_distance(min2
, max2
, min1
, max1
)
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
371 return (b1x
- b2x
+ 1j
* (b1y
- b2y
)) * 2 * minr2
/ (2 * r2
**1.5)
373 def attraction(self
, box
, wire
):
375 if box
== wire
.src
.module
:
377 b1
= wire
.src
.box
.get_bounds();
378 b2
= wire
.dest
.box
.get_bounds();
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
))
385 for m
in self
.modules
:
387 m
.bounds
= m
.group
.get_bounds()
390 cr
= self
.canvas
.create_cairo_context()
391 w
, h
= self
.canvas
.allocation
.width
, self
.canvas
.allocation
.height
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
:
404 force
+= temperature
* random
.random()
405 for m2
in self
.modules
:
408 force
+= self
.repulsion(m1
.bounds
, m2
.bounds
)
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
)
424 self
.canvas
.draw(gtk
.gdk
.Rectangle(0, 0, w
, h
))
425 print "Energy is %s" % energy