| ... | ... |
@@ -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 |