... | ... |
@@ -31,6 +31,7 @@ class Pen: |
31 | 31 |
"""Store pen attributes.""" |
32 | 32 |
|
33 | 33 |
def __init__(self): |
34 |
+ # set default attributes |
|
34 | 35 |
self.color = (0.0, 0.0, 0.0, 1.0) |
35 | 36 |
self.fillcolor = (0.0, 0.0, 0.0, 1.0) |
36 | 37 |
self.linewidth = 1.0 |
... | ... |
@@ -211,6 +212,74 @@ class CompoundShape(Shape): |
211 | 212 |
shape.draw(cr) |
212 | 213 |
|
213 | 214 |
|
215 |
+class Element(CompoundShape): |
|
216 |
+ """Base class for graph nodes and edges.""" |
|
217 |
+ |
|
218 |
+ def __init__(self, shapes): |
|
219 |
+ CompoundShape.__init__(self, shapes) |
|
220 |
+ |
|
221 |
+ def get_url(self, x, y): |
|
222 |
+ return None |
|
223 |
+ |
|
224 |
+ |
|
225 |
+class Node(Element): |
|
226 |
+ |
|
227 |
+ def __init__(self, x, y, w, h, shapes, url): |
|
228 |
+ Element.__init__(self, shapes) |
|
229 |
+ |
|
230 |
+ self.x1 = x - w/2 |
|
231 |
+ self.y1 = y - h/2 |
|
232 |
+ self.x2 = x + w/2 |
|
233 |
+ self.y2 = y + h/2 |
|
234 |
+ |
|
235 |
+ self.url = url |
|
236 |
+ |
|
237 |
+ def get_url(self, x, y): |
|
238 |
+ if self.url is None: |
|
239 |
+ return None |
|
240 |
+ #print (x, y), (self.x1, self.y1), "-", (self.x2, self.y2) |
|
241 |
+ if self.x1 <= x and x <= self.x2 and self.y1 <= y and y <= self.y2: |
|
242 |
+ return self.url |
|
243 |
+ return None |
|
244 |
+ |
|
245 |
+ |
|
246 |
+class Edge(Element): |
|
247 |
+ |
|
248 |
+ pass |
|
249 |
+ |
|
250 |
+ |
|
251 |
+class Graph(Shape): |
|
252 |
+ |
|
253 |
+ def __init__(self, width=1, height=1, nodes=(), edges=()): |
|
254 |
+ Shape.__init__(self) |
|
255 |
+ |
|
256 |
+ self.width = width |
|
257 |
+ self.height = height |
|
258 |
+ self.nodes = nodes |
|
259 |
+ self.edges = edges |
|
260 |
+ |
|
261 |
+ def get_size(self): |
|
262 |
+ return self.width, self.height |
|
263 |
+ |
|
264 |
+ def draw(self, cr): |
|
265 |
+ cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) |
|
266 |
+ |
|
267 |
+ cr.set_line_cap(cairo.LINE_CAP_BUTT) |
|
268 |
+ cr.set_line_join(cairo.LINE_JOIN_MITER) |
|
269 |
+ |
|
270 |
+ for edge in self.edges: |
|
271 |
+ edge.draw(cr) |
|
272 |
+ for node in self.nodes: |
|
273 |
+ node.draw(cr) |
|
274 |
+ |
|
275 |
+ def get_url(self, x, y): |
|
276 |
+ for node in self.nodes: |
|
277 |
+ url = node.get_url(x, y) |
|
278 |
+ if url is not None: |
|
279 |
+ return url |
|
280 |
+ return None |
|
281 |
+ |
|
282 |
+ |
|
214 | 283 |
class XDotAttrParser: |
215 | 284 |
"""Parser for xdot drawing attributes. |
216 | 285 |
See also: |
... | ... |
@@ -336,24 +405,69 @@ class XDotAttrParser: |
336 | 405 |
else: |
337 | 406 |
sys.stderr.write("unknown xdot opcode '%s'\n" % op) |
338 | 407 |
break |
339 |
- return CompoundShape(shapes) |
|
408 |
+ return shapes |
|
340 | 409 |
|
341 | 410 |
def transform(self, x, y): |
342 | 411 |
return self.parser.transform(x, y) |
343 | 412 |
|
344 | 413 |
|
345 |
-class Hyperlink: |
|
414 |
+class XDotParser: |
|
415 |
+ |
|
416 |
+ def __init__(self, xdotcode): |
|
417 |
+ self.xdotcode = xdotcode |
|
346 | 418 |
|
347 |
- def __init__(self, url, x, y, w, h): |
|
348 |
- self.url = url |
|
349 |
- self.x1 = x - w/2 |
|
350 |
- self.y1 = y - h/2 |
|
351 |
- self.x2 = x + w/2 |
|
352 |
- self.y2 = y + h/2 |
|
419 |
+ def parse(self): |
|
420 |
+ graph = pydot.graph_from_dot_data(self.xdotcode) |
|
353 | 421 |
|
354 |
- def hit(self, x, y): |
|
355 |
- #print (x, y), (self.x1, self.y1), "-", (self.x2, self.y2) |
|
356 |
- return self.x1 <= x and x <= self.x2 and self.y1 <= y and y <= self.y2 |
|
422 |
+ bb = graph.get_bb() |
|
423 |
+ if bb is None: |
|
424 |
+ return [] |
|
425 |
+ |
|
426 |
+ xmin, ymin, xmax, ymax = map(int, bb.split(",")) |
|
427 |
+ |
|
428 |
+ self.xoffset = -xmin |
|
429 |
+ self.yoffset = -ymax |
|
430 |
+ self.xscale = 1.0 |
|
431 |
+ self.yscale = -1.0 |
|
432 |
+ # FIXME: scale from points to pixels |
|
433 |
+ |
|
434 |
+ width = xmax - xmin |
|
435 |
+ height = ymax - ymin |
|
436 |
+ |
|
437 |
+ nodes = [] |
|
438 |
+ edges = [] |
|
439 |
+ |
|
440 |
+ for node in graph.get_node_list(): |
|
441 |
+ if node.pos is None: |
|
442 |
+ continue |
|
443 |
+ x, y = map(float, node.pos.split(",")) |
|
444 |
+ w = float(node.width)*72 |
|
445 |
+ h = float(node.height)*72 |
|
446 |
+ shapes = [] |
|
447 |
+ for attr in ("_draw_", "_ldraw_"): |
|
448 |
+ if hasattr(node, attr): |
|
449 |
+ parser = XDotAttrParser(self, getattr(node, attr)) |
|
450 |
+ shapes.extend(parser.parse()) |
|
451 |
+ url = node.URL |
|
452 |
+ if shapes: |
|
453 |
+ nodes.append(Node(x, y, w, h, shapes, url)) |
|
454 |
+ |
|
455 |
+ for edge in graph.get_edge_list(): |
|
456 |
+ shapes = [] |
|
457 |
+ for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"): |
|
458 |
+ if hasattr(edge, attr): |
|
459 |
+ parser = XDotAttrParser(self, getattr(edge, attr)) |
|
460 |
+ shapes.extend(parser.parse()) |
|
461 |
+ if shapes: |
|
462 |
+ edges.append(Edge(shapes)) |
|
463 |
+ |
|
464 |
+ return Graph(width, height, nodes, edges) |
|
465 |
+ |
|
466 |
+ def transform(self, x, y): |
|
467 |
+ # XXX: this is not the right place for this code |
|
468 |
+ x = (x + self.xoffset)*self.xscale |
|
469 |
+ y = (y + self.yoffset)*self.yscale |
|
470 |
+ return x, y |
|
357 | 471 |
|
358 | 472 |
|
359 | 473 |
class DotWindow(gtk.Window): |
... | ... |
@@ -374,11 +488,7 @@ class DotWindow(gtk.Window): |
374 | 488 |
def __init__(self): |
375 | 489 |
gtk.Window.__init__(self) |
376 | 490 |
|
377 |
- self.graph = None |
|
378 |
- self.width = 1 |
|
379 |
- self.height = 1 |
|
380 |
- self.shapes = [] |
|
381 |
- self.hyperlinks = [] |
|
491 |
+ self.graph = Graph() |
|
382 | 492 |
|
383 | 493 |
window = self |
384 | 494 |
|
... | ... |
@@ -416,14 +526,8 @@ class DotWindow(gtk.Window): |
416 | 526 |
toolbar = uimanager.get_widget('/ToolBar') |
417 | 527 |
vbox.pack_start(toolbar, False) |
418 | 528 |
|
419 |
- # TODO: Use a custom widget instead of Layout like in the scrollable.py example? |
|
420 |
- #scrolled_window = self.scrolled_window = gtk.ScrolledWindow() |
|
421 |
- #scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) |
|
422 |
- #vbox.pack_start(scrolled_window) |
|
423 |
- |
|
424 | 529 |
self.area = gtk.DrawingArea() |
425 | 530 |
self.area.connect("expose_event", self.on_expose) |
426 |
- #scrolled_window.add(self.area) |
|
427 | 531 |
vbox.pack_start(self.area) |
428 | 532 |
|
429 | 533 |
self.area.set_flags(gtk.CAN_FOCUS) |
... | ... |
@@ -440,7 +544,6 @@ class DotWindow(gtk.Window): |
440 | 544 |
|
441 | 545 |
self.x, self.y = 0.0, 0.0 |
442 | 546 |
self.zoom_ratio = 1.0 |
443 |
- self.pixbuf = None |
|
444 | 547 |
|
445 | 548 |
self.show_all() |
446 | 549 |
|
... | ... |
@@ -453,50 +556,13 @@ class DotWindow(gtk.Window): |
453 | 556 |
universal_newlines=True |
454 | 557 |
) |
455 | 558 |
xdotcode = p.communicate(dotcode)[0] |
456 |
- #sys.stdout.write(xdotcode) |
|
457 |
- self.parse(xdotcode) |
|
458 |
- self.zoom_image(self.zoom_ratio, center=True) |
|
459 |
- |
|
460 |
- def parse(self, xdotcode): |
|
461 |
- self.graph = pydot.graph_from_dot_data(xdotcode) |
|
559 |
+ self.set_xdotcode(xdotcode) |
|
462 | 560 |
|
463 |
- bb = self.graph.get_bb() |
|
464 |
- if bb is None: |
|
465 |
- return |
|
466 |
- |
|
467 |
- xmin, ymin, xmax, ymax = map(int, bb.split(",")) |
|
468 |
- |
|
469 |
- self.xoffset = -xmin |
|
470 |
- self.yoffset = -ymax |
|
471 |
- self.xscale = 1.0 |
|
472 |
- self.yscale = -1.0 |
|
473 |
- self.width = xmax - xmin |
|
474 |
- self.height = ymax - ymin |
|
475 |
- |
|
476 |
- self.shapes = [] |
|
477 |
- self.hyperlinks = [] |
|
478 |
- |
|
479 |
- for node in self.graph.get_node_list(): |
|
480 |
- for attr in ("_draw_", "_ldraw_"): |
|
481 |
- if hasattr(node, attr): |
|
482 |
- p = XDotAttrParser(self, getattr(node, attr)) |
|
483 |
- self.shapes.append(p.parse()) |
|
484 |
- if node.URL is not None: |
|
485 |
- x, y = map(float, node.pos.split(",")) |
|
486 |
- w = float(node.width)*72 |
|
487 |
- h = float(node.height)*72 |
|
488 |
- self.hyperlinks.append(Hyperlink(node.URL, x, y, w, h)) |
|
489 |
- for edge in self.graph.get_edge_list(): |
|
490 |
- for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"): |
|
491 |
- if hasattr(edge, attr): |
|
492 |
- p = XDotAttrParser(self, getattr(edge, attr)) |
|
493 |
- self.shapes.append(p.parse()) |
|
494 |
- |
|
495 |
- def transform(self, x, y): |
|
496 |
- # XXX: this is not the right place for this code |
|
497 |
- x = (x + self.xoffset)*self.xscale |
|
498 |
- y = (y + self.yoffset)*self.yscale |
|
499 |
- return x, y |
|
561 |
+ def set_xdotcode(self, xdotcode): |
|
562 |
+ #print xdotcode |
|
563 |
+ parser = XDotParser(xdotcode) |
|
564 |
+ self.graph = parser.parse() |
|
565 |
+ self.zoom_image(self.zoom_ratio, center=True) |
|
500 | 566 |
|
501 | 567 |
def on_expose(self, area, event): |
502 | 568 |
cr = area.window.cairo_create() |
... | ... |
@@ -516,15 +582,7 @@ class DotWindow(gtk.Window): |
516 | 582 |
cr.scale(self.zoom_ratio, self.zoom_ratio) |
517 | 583 |
cr.translate(-self.x, -self.y) |
518 | 584 |
|
519 |
- # FIXME: scale from points to pixels |
|
520 |
- |
|
521 |
- cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) |
|
522 |
- |
|
523 |
- cr.set_line_cap(cairo.LINE_CAP_BUTT) |
|
524 |
- cr.set_line_join(cairo.LINE_JOIN_MITER) |
|
525 |
- |
|
526 |
- for shape in self.shapes: |
|
527 |
- shape.draw(cr) |
|
585 |
+ self.graph.draw(cr) |
|
528 | 586 |
|
529 | 587 |
return False |
530 | 588 |
|
... | ... |
@@ -538,8 +596,8 @@ class DotWindow(gtk.Window): |
538 | 596 |
|
539 | 597 |
def zoom_image(self, zoom_ratio, center=False): |
540 | 598 |
if center: |
541 |
- self.x = self.width/2 |
|
542 |
- self.y = self.height/2 |
|
599 |
+ self.x = self.graph.width/2 |
|
600 |
+ self.y = self.graph.height/2 |
|
543 | 601 |
self.zoom_ratio = zoom_ratio |
544 | 602 |
self.area.queue_draw() |
545 | 603 |
|
... | ... |
@@ -554,8 +612,8 @@ class DotWindow(gtk.Window): |
554 | 612 |
def on_zoom_fit(self, action): |
555 | 613 |
rect = self.area.get_allocation() |
556 | 614 |
zoom_ratio = min( |
557 |
- float(rect.width)/float(self.width), |
|
558 |
- float(rect.height)/float(self.height) |
|
615 |
+ float(rect.width)/float(self.graph.width), |
|
616 |
+ float(rect.height)/float(self.graph.height) |
|
559 | 617 |
) |
560 | 618 |
self.zoom_image(zoom_ratio, center=True) |
561 | 619 |
|
... | ... |
@@ -651,12 +709,8 @@ class DotWindow(gtk.Window): |
651 | 709 |
y /= self.zoom_ratio |
652 | 710 |
x += self.x |
653 | 711 |
y += self.y |
654 |
- y = self.height - y |
|
655 |
- |
|
656 |
- for hyperlink in self.hyperlinks: |
|
657 |
- if hyperlink.hit(x, y): |
|
658 |
- return hyperlink.url |
|
659 |
- return None |
|
712 |
+ y = self.graph.height - y |
|
713 |
+ return self.graph.get_url(x, y) |
|
660 | 714 |
|
661 | 715 |
def on_url_clicked(self, url, event): |
662 | 716 |
return False |