Package rosh :: Package impl :: Module xdot
[frames] | no frames]

Source Code for Module rosh.impl.xdot

   1  #!/usr/bin/env python 
   2  # 
   3  # Copyright 2008 Jose Fonseca 
   4  # 
   5  # This program is free software: you can redistribute it and/or modify it 
   6  # under the terms of the GNU Lesser General Public License as published 
   7  # by the Free Software Foundation, either version 3 of the License, or 
   8  # (at your option) any later version. 
   9  # 
  10  # This program is distributed in the hope that it will be useful, 
  11  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
  12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  13  # GNU Lesser General Public License for more details. 
  14  # 
  15  # You should have received a copy of the GNU Lesser General Public License 
  16  # along with this program.  If not, see <http://www.gnu.org/licenses/>. 
  17  # 
  18   
  19  '''Visualize dot graphs via the xdot format.''' 
  20   
  21  __author__ = "Jose Fonseca" 
  22   
  23  __version__ = "0.4" 
  24   
  25   
  26  import os 
  27  import sys 
  28  import subprocess 
  29  import math 
  30  import colorsys 
  31  import time 
  32  import re 
  33   
  34  import gobject 
  35  import gtk 
  36  import gtk.gdk 
  37  import gtk.keysyms 
  38  import cairo 
  39  import pango 
  40  import pangocairo 
  41   
  42   
  43  # See http://www.graphviz.org/pub/scm/graphviz-cairo/plugin/cairo/gvrender_cairo.c 
  44   
  45  # For pygtk inspiration and guidance see: 
  46  # - http://mirageiv.berlios.de/ 
  47  # - http://comix.sourceforge.net/ 
  48   
  49   
50 -class Pen:
51 """Store pen attributes.""" 52
53 - def __init__(self):
54 # set default attributes 55 self.color = (0.0, 0.0, 0.0, 1.0) 56 self.fillcolor = (0.0, 0.0, 0.0, 1.0) 57 self.linewidth = 1.0 58 self.fontsize = 14.0 59 self.fontname = "Times-Roman" 60 self.dash = ()
61
62 - def copy(self):
63 """Create a copy of this pen.""" 64 pen = Pen() 65 pen.__dict__ = self.__dict__.copy() 66 return pen
67
68 - def highlighted(self):
69 pen = self.copy() 70 pen.color = (1, 0, 0, 1) 71 pen.fillcolor = (1, .8, .8, 1) 72 return pen
73 74
75 -class Shape:
76 """Abstract base class for all the drawing shapes.""" 77
78 - def __init__(self):
79 pass
80
81 - def draw(self, cr, highlight=False):
82 """Draw this shape with the given cairo context""" 83 raise NotImplementedError
84
85 - def select_pen(self, highlight):
86 if highlight: 87 if not hasattr(self, 'highlight_pen'): 88 self.highlight_pen = self.pen.highlighted() 89 return self.highlight_pen 90 else: 91 return self.pen
92 93
94 -class TextShape(Shape):
95 96 #fontmap = pangocairo.CairoFontMap() 97 #fontmap.set_resolution(72) 98 #context = fontmap.create_context() 99 100 LEFT, CENTER, RIGHT = -1, 0, 1 101
102 - def __init__(self, pen, x, y, j, w, t):
103 Shape.__init__(self) 104 self.pen = pen.copy() 105 self.x = x 106 self.y = y 107 self.j = j 108 self.w = w 109 self.t = t
110
111 - def draw(self, cr, highlight=False):
112 113 try: 114 layout = self.layout 115 except AttributeError: 116 layout = cr.create_layout() 117 118 # set font options 119 # see http://lists.freedesktop.org/archives/cairo/2007-February/009688.html 120 context = layout.get_context() 121 fo = cairo.FontOptions() 122 fo.set_antialias(cairo.ANTIALIAS_DEFAULT) 123 fo.set_hint_style(cairo.HINT_STYLE_NONE) 124 fo.set_hint_metrics(cairo.HINT_METRICS_OFF) 125 pangocairo.context_set_font_options(context, fo) 126 127 # set font 128 font = pango.FontDescription() 129 font.set_family(self.pen.fontname) 130 font.set_absolute_size(self.pen.fontsize*pango.SCALE) 131 layout.set_font_description(font) 132 133 # set text 134 layout.set_text(self.t) 135 136 # cache it 137 self.layout = layout 138 else: 139 cr.update_layout(layout) 140 141 descent = 2 # XXX get descender from font metrics 142 143 width, height = layout.get_size() 144 width = float(width)/pango.SCALE 145 height = float(height)/pango.SCALE 146 # we know the width that dot thinks this text should have 147 # we do not necessarily have a font with the same metrics 148 # scale it so that the text fits inside its box 149 if width > self.w: 150 f = self.w / width 151 width = self.w # equivalent to width *= f 152 height *= f 153 descent *= f 154 else: 155 f = 1.0 156 157 if self.j == self.LEFT: 158 x = self.x 159 elif self.j == self.CENTER: 160 x = self.x - 0.5*width 161 elif self.j == self.RIGHT: 162 x = self.x - width 163 else: 164 assert 0 165 166 y = self.y - height + descent 167 168 cr.move_to(x, y) 169 170 cr.save() 171 cr.scale(f, f) 172 cr.set_source_rgba(*self.select_pen(highlight).color) 173 cr.show_layout(layout) 174 cr.restore() 175 176 if 0: # DEBUG 177 # show where dot thinks the text should appear 178 cr.set_source_rgba(1, 0, 0, .9) 179 if self.j == self.LEFT: 180 x = self.x 181 elif self.j == self.CENTER: 182 x = self.x - 0.5*self.w 183 elif self.j == self.RIGHT: 184 x = self.x - self.w 185 cr.move_to(x, self.y) 186 cr.line_to(x+self.w, self.y) 187 cr.stroke()
188 189
190 -class EllipseShape(Shape):
191
192 - def __init__(self, pen, x0, y0, w, h, filled=False):
193 Shape.__init__(self) 194 self.pen = pen.copy() 195 self.x0 = x0 196 self.y0 = y0 197 self.w = w 198 self.h = h 199 self.filled = filled
200
201 - def draw(self, cr, highlight=False):
202 cr.save() 203 cr.translate(self.x0, self.y0) 204 cr.scale(self.w, self.h) 205 cr.move_to(1.0, 0.0) 206 cr.arc(0.0, 0.0, 1.0, 0, 2.0*math.pi) 207 cr.restore() 208 pen = self.select_pen(highlight) 209 if self.filled: 210 cr.set_source_rgba(*pen.fillcolor) 211 cr.fill() 212 else: 213 cr.set_dash(pen.dash) 214 cr.set_line_width(pen.linewidth) 215 cr.set_source_rgba(*pen.color) 216 cr.stroke()
217 218
219 -class PolygonShape(Shape):
220
221 - def __init__(self, pen, points, filled=False):
222 Shape.__init__(self) 223 self.pen = pen.copy() 224 self.points = points 225 self.filled = filled
226
227 - def draw(self, cr, highlight=False):
228 x0, y0 = self.points[-1] 229 cr.move_to(x0, y0) 230 for x, y in self.points: 231 cr.line_to(x, y) 232 cr.close_path() 233 pen = self.select_pen(highlight) 234 if self.filled: 235 cr.set_source_rgba(*pen.fillcolor) 236 cr.fill_preserve() 237 cr.fill() 238 else: 239 cr.set_dash(pen.dash) 240 cr.set_line_width(pen.linewidth) 241 cr.set_source_rgba(*pen.color) 242 cr.stroke()
243 244
245 -class LineShape(Shape):
246
247 - def __init__(self, pen, points):
248 Shape.__init__(self) 249 self.pen = pen.copy() 250 self.points = points
251
252 - def draw(self, cr, highlight=False):
253 x0, y0 = self.points[0] 254 cr.move_to(x0, y0) 255 for x1, y1 in self.points[1:]: 256 cr.line_to(x1, y1) 257 pen = self.select_pen(highlight) 258 cr.set_dash(pen.dash) 259 cr.set_line_width(pen.linewidth) 260 cr.set_source_rgba(*pen.color) 261 cr.stroke()
262 263
264 -class BezierShape(Shape):
265
266 - def __init__(self, pen, points):
267 Shape.__init__(self) 268 self.pen = pen.copy() 269 self.points = points
270
271 - def draw(self, cr, highlight=False):
272 x0, y0 = self.points[0] 273 cr.move_to(x0, y0) 274 for i in xrange(1, len(self.points), 3): 275 x1, y1 = self.points[i] 276 x2, y2 = self.points[i + 1] 277 x3, y3 = self.points[i + 2] 278 cr.curve_to(x1, y1, x2, y2, x3, y3) 279 pen = self.select_pen(highlight) 280 cr.set_dash(pen.dash) 281 cr.set_line_width(pen.linewidth) 282 cr.set_source_rgba(*pen.color) 283 cr.stroke()
284 285
286 -class CompoundShape(Shape):
287
288 - def __init__(self, shapes):
289 Shape.__init__(self) 290 self.shapes = shapes
291
292 - def draw(self, cr, highlight=False):
293 for shape in self.shapes: 294 shape.draw(cr, highlight=highlight)
295 296
297 -class Url(object):
298
299 - def __init__(self, item, url, highlight=None):
300 self.item = item 301 self.url = url 302 if highlight is None: 303 highlight = set([item]) 304 self.highlight = highlight
305 306
307 -class Jump(object):
308
309 - def __init__(self, item, x, y, highlight=None):
310 self.item = item 311 self.x = x 312 self.y = y 313 if highlight is None: 314 highlight = set([item]) 315 self.highlight = highlight
316 317
318 -class Element(CompoundShape):
319 """Base class for graph nodes and edges.""" 320
321 - def __init__(self, shapes):
322 CompoundShape.__init__(self, shapes)
323
324 - def get_url(self, x, y):
325 return None
326
327 - def get_jump(self, x, y):
328 return None
329 330
331 -class Node(Element):
332
333 - def __init__(self, x, y, w, h, shapes, url):
334 Element.__init__(self, shapes) 335 336 self.x = x 337 self.y = y 338 339 self.x1 = x - 0.5*w 340 self.y1 = y - 0.5*h 341 self.x2 = x + 0.5*w 342 self.y2 = y + 0.5*h 343 344 self.url = url
345
346 - def is_inside(self, x, y):
347 return self.x1 <= x and x <= self.x2 and self.y1 <= y and y <= self.y2
348
349 - def get_url(self, x, y):
350 if self.url is None: 351 return None 352 #print (x, y), (self.x1, self.y1), "-", (self.x2, self.y2) 353 if self.is_inside(x, y): 354 return Url(self, self.url) 355 return None
356
357 - def get_jump(self, x, y):
358 if self.is_inside(x, y): 359 return Jump(self, self.x, self.y) 360 return None
361 362
363 -def square_distance(x1, y1, x2, y2):
364 deltax = x2 - x1 365 deltay = y2 - y1 366 return deltax*deltax + deltay*deltay
367 368
369 -class Edge(Element):
370
371 - def __init__(self, src, dst, points, shapes):
372 Element.__init__(self, shapes) 373 self.src = src 374 self.dst = dst 375 self.points = points
376 377 RADIUS = 10 378
379 - def get_jump(self, x, y):
380 if square_distance(x, y, *self.points[0]) <= self.RADIUS*self.RADIUS: 381 return Jump(self, self.dst.x, self.dst.y, highlight=set([self, self.dst])) 382 if square_distance(x, y, *self.points[-1]) <= self.RADIUS*self.RADIUS: 383 return Jump(self, self.src.x, self.src.y, highlight=set([self, self.src])) 384 return None
385 386
387 -class Graph(Shape):
388
389 - def __init__(self, width=1, height=1, nodes=(), edges=()):
390 Shape.__init__(self) 391 392 self.width = width 393 self.height = height 394 self.nodes = nodes 395 self.edges = edges
396
397 - def get_size(self):
398 return self.width, self.height
399
400 - def draw(self, cr, highlight_items=None):
401 if highlight_items is None: 402 highlight_items = () 403 cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) 404 405 cr.set_line_cap(cairo.LINE_CAP_BUTT) 406 cr.set_line_join(cairo.LINE_JOIN_MITER) 407 408 for edge in self.edges: 409 edge.draw(cr, highlight=(edge in highlight_items)) 410 for node in self.nodes: 411 node.draw(cr, highlight=(node in highlight_items))
412
413 - def get_url(self, x, y):
414 for node in self.nodes: 415 url = node.get_url(x, y) 416 if url is not None: 417 return url 418 return None
419
420 - def get_jump(self, x, y):
421 for edge in self.edges: 422 jump = edge.get_jump(x, y) 423 if jump is not None: 424 return jump 425 for node in self.nodes: 426 jump = node.get_jump(x, y) 427 if jump is not None: 428 return jump 429 return None
430 431
432 -class XDotAttrParser:
433 """Parser for xdot drawing attributes. 434 See also: 435 - http://www.graphviz.org/doc/info/output.html#d:xdot 436 """ 437
438 - def __init__(self, parser, buf):
439 self.parser = parser 440 self.buf = self.unescape(buf) 441 self.pos = 0
442
443 - def __nonzero__(self):
444 return self.pos < len(self.buf)
445
446 - def unescape(self, buf):
447 buf = buf.replace('\\"', '"') 448 buf = buf.replace('\\n', '\n') 449 return buf
450
451 - def read_code(self):
452 pos = self.buf.find(" ", self.pos) 453 res = self.buf[self.pos:pos] 454 self.pos = pos + 1 455 while self.pos < len(self.buf) and self.buf[self.pos].isspace(): 456 self.pos += 1 457 return res
458
459 - def read_number(self):
460 return int(self.read_code())
461
462 - def read_float(self):
463 return float(self.read_code())
464
465 - def read_point(self):
466 x = self.read_number() 467 y = self.read_number() 468 return self.transform(x, y)
469
470 - def read_text(self):
471 num = self.read_number() 472 pos = self.buf.find("-", self.pos) + 1 473 self.pos = pos + num 474 res = self.buf[pos:self.pos] 475 while self.pos < len(self.buf) and self.buf[self.pos].isspace(): 476 self.pos += 1 477 return res
478
479 - def read_polygon(self):
480 n = self.read_number() 481 p = [] 482 for i in range(n): 483 x, y = self.read_point() 484 p.append((x, y)) 485 return p
486
487 - def read_color(self, fallback=(0,0,0,1)):
488 # See http://www.graphviz.org/doc/info/attrs.html#k:color 489 c = self.read_text() 490 c1 = c[:1] 491 if c1 == '#': 492 hex2float = lambda h: float(int(h, 16)/255.0) 493 r = hex2float(c[1:3]) 494 g = hex2float(c[3:5]) 495 b = hex2float(c[5:7]) 496 try: 497 a = hex2float(c[7:9]) 498 except (IndexError, ValueError): 499 a = 1.0 500 return r, g, b, a 501 elif c1.isdigit() or c1 == ".": 502 # "H,S,V" or "H S V" or "H, S, V" or any other variation 503 h, s, v = map(float, c.replace(",", " ").split()) 504 r, g, b = colorsys.hsv_to_rgb(h, s, v) 505 a = 1.0 506 return r, g, b, a 507 else: 508 try: 509 color = gtk.gdk.color_parse(c) 510 except ValueError: 511 sys.stderr.write("unknown color '%s'\n" % c) 512 return fallback 513 s = 1.0/65535.0 514 r = color.red*s 515 g = color.green*s 516 b = color.blue*s 517 a = 1.0 518 return r, g, b, a
519
520 - def parse(self):
521 shapes = [] 522 pen = Pen() 523 s = self 524 525 while s: 526 op = s.read_code() 527 if op == "c": 528 pen.color = s.read_color(fallback=pen.color) 529 elif op == "C": 530 pen.fillcolor = s.read_color(fallback=pen.fillcolor) 531 elif op == "S": 532 style = s.read_text() 533 if style.startswith("setlinewidth("): 534 lw = style.split("(")[1].split(")")[0] 535 pen.linewidth = float(lw) 536 elif style == "solid": 537 pen.dash = () 538 elif style == "dashed": 539 pen.dash = (6, ) # 6pt on, 6pt off 540 elif op == "F": 541 pen.fontsize = s.read_float() 542 pen.fontname = s.read_text() 543 elif op == "T": 544 x, y = s.read_point() 545 j = s.read_number() 546 w = s.read_number() 547 t = s.read_text() 548 shapes.append(TextShape(pen, x, y, j, w, t)) 549 elif op == "E": 550 x0, y0 = s.read_point() 551 w = s.read_number() 552 h = s.read_number() 553 # xdot uses this to mean "draw a filled shape with an outline" 554 shapes.append(EllipseShape(pen, x0, y0, w, h, filled=True)) 555 shapes.append(EllipseShape(pen, x0, y0, w, h)) 556 elif op == "e": 557 x0, y0 = s.read_point() 558 w = s.read_number() 559 h = s.read_number() 560 shapes.append(EllipseShape(pen, x0, y0, w, h)) 561 elif op == "L": 562 p = self.read_polygon() 563 shapes.append(LineShape(pen, p)) 564 elif op == "B": 565 p = self.read_polygon() 566 shapes.append(BezierShape(pen, p)) 567 elif op == "P": 568 p = self.read_polygon() 569 # xdot uses this to mean "draw a filled shape with an outline" 570 shapes.append(PolygonShape(pen, p, filled=True)) 571 shapes.append(PolygonShape(pen, p)) 572 elif op == "p": 573 p = self.read_polygon() 574 shapes.append(PolygonShape(pen, p)) 575 else: 576 sys.stderr.write("unknown xdot opcode '%s'\n" % op) 577 break 578 return shapes
579
580 - def transform(self, x, y):
581 return self.parser.transform(x, y)
582 583 584 EOF = -1 585 SKIP = -2 586 587
588 -class ParseError(Exception):
589
590 - def __init__(self, msg=None, filename=None, line=None, col=None):
591 self.msg = msg 592 self.filename = filename 593 self.line = line 594 self.col = col
595
596 - def __str__(self):
597 return ':'.join([str(part) for part in (self.filename, self.line, self.col, self.msg) if part != None])
598 599
600 -class Scanner:
601 """Stateless scanner.""" 602 603 # should be overriden by derived classes 604 tokens = [] 605 symbols = {} 606 literals = {} 607 ignorecase = False 608
609 - def __init__(self):
610 flags = re.DOTALL 611 if self.ignorecase: 612 flags |= re.IGNORECASE 613 self.tokens_re = re.compile( 614 '|'.join(['(' + regexp + ')' for type, regexp, test_lit in self.tokens]), 615 flags 616 )
617
618 - def next(self, buf, pos):
619 if pos >= len(buf): 620 return EOF, '', pos 621 mo = self.tokens_re.match(buf, pos) 622 if mo: 623 text = mo.group() 624 type, regexp, test_lit = self.tokens[mo.lastindex - 1] 625 pos = mo.end() 626 if test_lit: 627 type = self.literals.get(text, type) 628 return type, text, pos 629 else: 630 c = buf[pos] 631 return self.symbols.get(c, None), c, pos + 1
632 633
634 -class Token:
635
636 - def __init__(self, type, text, line, col):
637 self.type = type 638 self.text = text 639 self.line = line 640 self.col = col
641 642
643 -class Lexer:
644 645 # should be overriden by derived classes 646 scanner = None 647 tabsize = 8 648 649 newline_re = re.compile(r'\r\n?|\n') 650
651 - def __init__(self, buf = None, pos = 0, filename = None, fp = None):
652 if fp is not None: 653 try: 654 fileno = fp.fileno() 655 length = os.path.getsize(fp.name) 656 import mmap 657 except: 658 # read whole file into memory 659 buf = fp.read() 660 pos = 0 661 else: 662 # map the whole file into memory 663 if length: 664 # length must not be zero 665 buf = mmap.mmap(fileno, length, access = mmap.ACCESS_READ) 666 pos = os.lseek(fileno, 0, 1) 667 else: 668 buf = '' 669 pos = 0 670 671 if filename is None: 672 try: 673 filename = fp.name 674 except AttributeError: 675 filename = None 676 677 self.buf = buf 678 self.pos = pos 679 self.line = 1 680 self.col = 1 681 self.filename = filename
682
683 - def next(self):
684 while True: 685 # save state 686 pos = self.pos 687 line = self.line 688 col = self.col 689 690 type, text, endpos = self.scanner.next(self.buf, pos) 691 assert pos + len(text) == endpos 692 self.consume(text) 693 type, text = self.filter(type, text) 694 self.pos = endpos 695 696 if type == SKIP: 697 continue 698 elif type is None: 699 msg = 'unexpected char ' 700 if text >= ' ' and text <= '~': 701 msg += "'%s'" % text 702 else: 703 msg += "0x%X" % ord(text) 704 raise ParseError(msg, self.filename, line, col) 705 else: 706 break 707 return Token(type = type, text = text, line = line, col = col)
708
709 - def consume(self, text):
710 # update line number 711 pos = 0 712 for mo in self.newline_re.finditer(text, pos): 713 self.line += 1 714 self.col = 1 715 pos = mo.end() 716 717 # update column number 718 while True: 719 tabpos = text.find('\t', pos) 720 if tabpos == -1: 721 break 722 self.col += tabpos - pos 723 self.col = ((self.col - 1)//self.tabsize + 1)*self.tabsize + 1 724 pos = tabpos + 1 725 self.col += len(text) - pos
726 727
728 -class Parser:
729
730 - def __init__(self, lexer):
731 self.lexer = lexer 732 self.lookahead = self.lexer.next()
733
734 - def match(self, type):
735 if self.lookahead.type != type: 736 raise ParseError( 737 msg = 'unexpected token %r' % self.lookahead.text, 738 filename = self.lexer.filename, 739 line = self.lookahead.line, 740 col = self.lookahead.col)
741
742 - def skip(self, type):
743 while self.lookahead.type != type: 744 self.consume()
745
746 - def consume(self):
747 token = self.lookahead 748 self.lookahead = self.lexer.next() 749 return token
750 751 752 ID = 0 753 STR_ID = 1 754 HTML_ID = 2 755 EDGE_OP = 3 756 757 LSQUARE = 4 758 RSQUARE = 5 759 LCURLY = 6 760 RCURLY = 7 761 COMMA = 8 762 COLON = 9 763 SEMI = 10 764 EQUAL = 11 765 PLUS = 12 766 767 STRICT = 13 768 GRAPH = 14 769 DIGRAPH = 15 770 NODE = 16 771 EDGE = 17 772 SUBGRAPH = 18 773 774
775 -class DotScanner(Scanner):
776 777 # token regular expression table 778 tokens = [ 779 # whitespace and comments 780 (SKIP, 781 r'[ \t\f\r\n\v]+|' 782 r'//[^\r\n]*|' 783 r'/\*.*?\*/|' 784 r'#[^\r\n]*', 785 False), 786 787 # Alphanumeric IDs 788 (ID, r'[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*', True), 789 790 # Numeric IDs 791 (ID, r'-?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)', False), 792 793 # String IDs 794 (STR_ID, r'"[^"\\]*(?:\\.[^"\\]*)*"', False), 795 796 # HTML IDs 797 (HTML_ID, r'<[^<>]*(?:<[^<>]*>[^<>]*)*>', False), 798 799 # Edge operators 800 (EDGE_OP, r'-[>-]', False), 801 ] 802 803 # symbol table 804 symbols = { 805 '[': LSQUARE, 806 ']': RSQUARE, 807 '{': LCURLY, 808 '}': RCURLY, 809 ',': COMMA, 810 ':': COLON, 811 ';': SEMI, 812 '=': EQUAL, 813 '+': PLUS, 814 } 815 816 # literal table 817 literals = { 818 'strict': STRICT, 819 'graph': GRAPH, 820 'digraph': DIGRAPH, 821 'node': NODE, 822 'edge': EDGE, 823 'subgraph': SUBGRAPH, 824 } 825 826 ignorecase = True
827 828
829 -class DotLexer(Lexer):
830 831 scanner = DotScanner() 832
833 - def filter(self, type, text):
834 # TODO: handle charset 835 if type == STR_ID: 836 text = text[1:-1] 837 838 # line continuations 839 text = text.replace('\\\r\n', '') 840 text = text.replace('\\\r', '') 841 text = text.replace('\\\n', '') 842 843 text = text.replace('\\r', '\r') 844 text = text.replace('\\n', '\n') 845 text = text.replace('\\t', '\t') 846 text = text.replace('\\', '') 847 848 type = ID 849 850 elif type == HTML_ID: 851 text = text[1:-1] 852 type = ID 853 854 return type, text
855 856
857 -class DotParser(Parser):
858
859 - def __init__(self, lexer):
860 Parser.__init__(self, lexer) 861 self.graph_attrs = {} 862 self.node_attrs = {} 863 self.edge_attrs = {}
864
865 - def parse(self):
866 self.parse_graph() 867 self.match(EOF)
868
869 - def parse_graph(self):
870 if self.lookahead.type == STRICT: 871 self.consume() 872 self.skip(LCURLY) 873 self.consume() 874 while self.lookahead.type != RCURLY: 875 self.parse_stmt() 876 self.consume()
877
878 - def parse_subgraph(self):
879 id = None 880 if self.lookahead.type == SUBGRAPH: 881 self.consume() 882 if self.lookahead.type == ID: 883 id = self.lookahead.text 884 self.consume() 885 if self.lookahead.type == LCURLY: 886 self.consume() 887 while self.lookahead.type != RCURLY: 888 self.parse_stmt() 889 self.consume() 890 return id
891
892 - def parse_stmt(self):
893 if self.lookahead.type == GRAPH: 894 self.consume() 895 attrs = self.parse_attrs() 896 self.graph_attrs.update(attrs) 897 self.handle_graph(attrs) 898 elif self.lookahead.type == NODE: 899 self.consume() 900 self.node_attrs.update(self.parse_attrs()) 901 elif self.lookahead.type == EDGE: 902 self.consume() 903 self.edge_attrs.update(self.parse_attrs()) 904 elif self.lookahead.type in (SUBGRAPH, LCURLY): 905 self.parse_subgraph() 906 else: 907 id = self.parse_node_id() 908 if self.lookahead.type == EDGE_OP: 909 self.consume() 910 node_ids = [id, self.parse_node_id()] 911 while self.lookahead.type == EDGE_OP: 912 node_ids.append(self.parse_node_id()) 913 attrs = self.parse_attrs() 914 for i in range(0, len(node_ids) - 1): 915 self.handle_edge(node_ids[i], node_ids[i + 1], attrs) 916 elif self.lookahead.type == EQUAL: 917 self.consume() 918 self.parse_id() 919 else: 920 attrs = self.parse_attrs() 921 self.handle_node(id, attrs) 922 if self.lookahead.type == SEMI: 923 self.consume()
924
925 - def parse_attrs(self):
926 attrs = {} 927 while self.lookahead.type == LSQUARE: 928 self.consume() 929 while self.lookahead.type != RSQUARE: 930 name, value = self.parse_attr() 931 attrs[name] = value 932 if self.lookahead.type == COMMA: 933 self.consume() 934 self.consume() 935 return attrs
936
937 - def parse_attr(self):
938 name = self.parse_id() 939 if self.lookahead.type == EQUAL: 940 self.consume() 941 value = self.parse_id() 942 else: 943 value = 'true' 944 return name, value
945
946 - def parse_node_id(self):
947 node_id = self.parse_id() 948 if self.lookahead.type == COLON: 949 self.consume() 950 port = self.parse_id() 951 if self.lookahead.type == COLON: 952 self.consume() 953 compass_pt = self.parse_id() 954 else: 955 compass_pt = None 956 else: 957 port = None 958 compass_pt = None 959 # XXX: we don't really care about port and compass point values when parsing xdot 960 return node_id
961
962 - def parse_id(self):
963 self.match(ID) 964 id = self.lookahead.text 965 self.consume() 966 return id
967
968 - def handle_graph(self, attrs):
969 pass
970
971 - def handle_node(self, id, attrs):
972 pass
973
974 - def handle_edge(self, src_id, dst_id, attrs):
975 pass
976 977
978 -class XDotParser(DotParser):
979
980 - def __init__(self, xdotcode):
981 lexer = DotLexer(buf = xdotcode) 982 DotParser.__init__(self, lexer) 983 984 self.nodes = [] 985 self.edges = [] 986 self.node_by_name = {}
987
988 - def handle_graph(self, attrs):
989 try: 990 bb = attrs['bb'] 991 except KeyError: 992 return 993 994 if not bb: 995 return 996 997 xmin, ymin, xmax, ymax = map(int, bb.split(",")) 998 999 self.xoffset = -xmin 1000 self.yoffset = -ymax 1001 self.xscale = 1.0 1002 self.yscale = -1.0 1003 # FIXME: scale from points to pixels 1004 1005 self.width = xmax - xmin 1006 self.height = ymax - ymin
1007
1008 - def handle_node(self, id, attrs):
1009 try: 1010 pos = attrs['pos'] 1011 except KeyError: 1012 return 1013 1014 x, y = self.parse_node_pos(pos) 1015 w = float(attrs['width'])*72 1016 h = float(attrs['height'])*72 1017 shapes = [] 1018 for attr in ("_draw_", "_ldraw_"): 1019 if attr in attrs: 1020 parser = XDotAttrParser(self, attrs[attr]) 1021 shapes.extend(parser.parse()) 1022 url = attrs.get('URL', None) 1023 node = Node(x, y, w, h, shapes, url) 1024 self.node_by_name[id] = node 1025 if shapes: 1026 self.nodes.append(node)
1027
1028 - def handle_edge(self, src_id, dst_id, attrs):
1029 try: 1030 pos = attrs['pos'] 1031 except KeyError: 1032 return 1033 1034 points = self.parse_edge_pos(pos) 1035 shapes = [] 1036 for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"): 1037 if attr in attrs: 1038 parser = XDotAttrParser(self, attrs[attr]) 1039 shapes.extend(parser.parse()) 1040 if shapes: 1041 src = self.node_by_name[src_id] 1042 dst = self.node_by_name[dst_id] 1043 self.edges.append(Edge(src, dst, points, shapes))
1044
1045 - def parse(self):
1046 DotParser.parse(self) 1047 1048 return Graph(self.width, self.height, self.nodes, self.edges)
1049
1050 - def parse_node_pos(self, pos):
1051 x, y = pos.split(",") 1052 return self.transform(float(x), float(y))
1053
1054 - def parse_edge_pos(self, pos):
1055 points = [] 1056 for entry in pos.split(' '): 1057 fields = entry.split(',') 1058 try: 1059 x, y = fields 1060 except ValueError: 1061 # TODO: handle start/end points 1062 continue 1063 else: 1064 points.append(self.transform(float(x), float(y))) 1065 return points
1066
1067 - def transform(self, x, y):
1068 # XXX: this is not the right place for this code 1069 x = (x + self.xoffset)*self.xscale 1070 y = (y + self.yoffset)*self.yscale 1071 return x, y
1072 1073
1074 -class Animation(object):
1075 1076 step = 0.03 # seconds 1077
1078 - def __init__(self, dot_widget):
1079 self.dot_widget = dot_widget 1080 self.timeout_id = None
1081
1082 - def start(self):
1083 self.timeout_id = gobject.timeout_add(int(self.step * 1000), self.tick)
1084
1085 - def stop(self):
1086 self.dot_widget.animation = NoAnimation(self.dot_widget) 1087 if self.timeout_id is not None: 1088 gobject.source_remove(self.timeout_id) 1089 self.timeout_id = None
1090
1091 - def tick(self):
1092 self.stop()
1093 1094
1095 -class NoAnimation(Animation):
1096
1097 - def start(self):
1098 pass
1099
1100 - def stop(self):
1101 pass
1102 1103
1104 -class LinearAnimation(Animation):
1105 1106 duration = 0.6 1107
1108 - def start(self):
1109 self.started = time.time() 1110 Animation.start(self)
1111
1112 - def tick(self):
1113 t = (time.time() - self.started) / self.duration 1114 self.animate(max(0, min(t, 1))) 1115 return (t < 1)
1116
1117 - def animate(self, t):
1118 pass
1119 1120
1121 -class MoveToAnimation(LinearAnimation):
1122
1123 - def __init__(self, dot_widget, target_x, target_y):
1124 Animation.__init__(self, dot_widget) 1125 self.source_x = dot_widget.x 1126 self.source_y = dot_widget.y 1127 self.target_x = target_x 1128 self.target_y = target_y
1129
1130 - def animate(self, t):
1131 sx, sy = self.source_x, self.source_y 1132 tx, ty = self.target_x, self.target_y 1133 self.dot_widget.x = tx * t + sx * (1-t) 1134 self.dot_widget.y = ty * t + sy * (1-t) 1135 self.dot_widget.queue_draw()
1136 1137
1138 -class ZoomToAnimation(MoveToAnimation):
1139
1140 - def __init__(self, dot_widget, target_x, target_y):
1141 MoveToAnimation.__init__(self, dot_widget, target_x, target_y) 1142 self.source_zoom = dot_widget.zoom_ratio 1143 self.target_zoom = self.source_zoom 1144 self.extra_zoom = 0 1145 1146 middle_zoom = 0.5 * (self.source_zoom + self.target_zoom) 1147 1148 distance = math.hypot(self.source_x - self.target_x, 1149 self.source_y - self.target_y) 1150 rect = self.dot_widget.get_allocation() 1151 visible = min(rect.width, rect.height) / self.dot_widget.zoom_ratio 1152 visible *= 0.9 1153 if distance > 0: 1154 desired_middle_zoom = visible / distance 1155 self.extra_zoom = min(0, 4 * (desired_middle_zoom - middle_zoom))
1156
1157 - def animate(self, t):
1158 a, b, c = self.source_zoom, self.extra_zoom, self.target_zoom 1159 self.dot_widget.zoom_ratio = c*t + b*t*(1-t) + a*(1-t) 1160 self.dot_widget.zoom_to_fit_on_resize = False 1161 MoveToAnimation.animate(self, t)
1162 1163
1164 -class DragAction(object):
1165
1166 - def __init__(self, dot_widget):
1167 self.dot_widget = dot_widget
1168
1169 - def on_button_press(self, event):
1170 self.startmousex = self.prevmousex = event.x 1171 self.startmousey = self.prevmousey = event.y 1172 self.start()
1173
1174 - def on_motion_notify(self, event):
1175 deltax = self.prevmousex - event.x 1176 deltay = self.prevmousey - event.y 1177 self.drag(deltax, deltay) 1178 self.prevmousex = event.x 1179 self.prevmousey = event.y
1180
1181 - def on_button_release(self, event):
1182 self.stopmousex = event.x 1183 self.stopmousey = event.y 1184 self.stop()
1185
1186 - def draw(self, cr):
1187 pass
1188
1189 - def start(self):
1190 pass
1191
1192 - def drag(self, deltax, deltay):
1193 pass
1194
1195 - def stop(self):
1196 pass
1197
1198 - def abort(self):
1199 pass
1200 1201
1202 -class NullAction(DragAction):
1203
1204 - def on_motion_notify(self, event):
1205 dot_widget = self.dot_widget 1206 item = dot_widget.get_url(event.x, event.y) 1207 if item is None: 1208 item = dot_widget.get_jump(event.x, event.y) 1209 if item is not None: 1210 dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2)) 1211 dot_widget.set_highlight(item.highlight) 1212 else: 1213 dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW)) 1214 dot_widget.set_highlight(None)
1215 1216
1217 -class PanAction(DragAction):
1218
1219 - def start(self):
1220 self.dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
1221
1222 - def drag(self, deltax, deltay):
1223 self.dot_widget.x += deltax / self.dot_widget.zoom_ratio 1224 self.dot_widget.y += deltay / self.dot_widget.zoom_ratio 1225 self.dot_widget.queue_draw()
1226
1227 - def stop(self):
1228 self.dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
1229 1230 abort = stop
1231 1232
1233 -class ZoomAction(DragAction):
1234
1235 - def drag(self, deltax, deltay):
1236 self.dot_widget.zoom_ratio *= 1.005 ** (deltax + deltay) 1237 self.dot_widget.zoom_to_fit_on_resize = False 1238 self.dot_widget.queue_draw()
1239
1240 - def stop(self):
1241 self.dot_widget.queue_draw()
1242 1243
1244 -class ZoomAreaAction(DragAction):
1245
1246 - def drag(self, deltax, deltay):
1247 self.dot_widget.queue_draw()
1248
1249 - def draw(self, cr):
1250 cr.save() 1251 cr.set_source_rgba(.5, .5, 1.0, 0.25) 1252 cr.rectangle(self.startmousex, self.startmousey, 1253 self.prevmousex - self.startmousex, 1254 self.prevmousey - self.startmousey) 1255 cr.fill() 1256 cr.set_source_rgba(.5, .5, 1.0, 1.0) 1257 cr.set_line_width(1) 1258 cr.rectangle(self.startmousex - .5, self.startmousey - .5, 1259 self.prevmousex - self.startmousex + 1, 1260 self.prevmousey - self.startmousey + 1) 1261 cr.stroke() 1262 cr.restore()
1263
1264 - def stop(self):
1265 x1, y1 = self.dot_widget.window2graph(self.startmousex, 1266 self.startmousey) 1267 x2, y2 = self.dot_widget.window2graph(self.stopmousex, 1268 self.stopmousey) 1269 self.dot_widget.zoom_to_area(x1, y1, x2, y2)
1270
1271 - def abort(self):
1272 self.dot_widget.queue_draw()
1273 1274
1275 -class DotWidget(gtk.DrawingArea):
1276 """PyGTK widget that draws dot graphs.""" 1277 1278 __gsignals__ = { 1279 'expose-event': 'override', 1280 'clicked' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gtk.gdk.Event)) 1281 } 1282 1283 filter = 'dot' 1284
1285 - def __init__(self):
1286 gtk.DrawingArea.__init__(self) 1287 1288 self.graph = Graph() 1289 1290 self.set_flags(gtk.CAN_FOCUS) 1291 1292 self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK) 1293 self.connect("button-press-event", self.on_area_button_press) 1294 self.connect("button-release-event", self.on_area_button_release) 1295 self.add_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK) 1296 self.connect("motion-notify-event", self.on_area_motion_notify) 1297 self.connect("scroll-event", self.on_area_scroll_event) 1298 self.connect("size-allocate", self.on_area_size_allocate) 1299 1300 self.connect('key-press-event', self.on_key_press_event) 1301 1302 self.x, self.y = 0.0, 0.0 1303 self.zoom_ratio = 1.0 1304 self.zoom_to_fit_on_resize = False 1305 self.animation = NoAnimation(self) 1306 self.drag_action = NullAction(self) 1307 self.presstime = None 1308 self.highlight = None 1309 # ADDITIONS ##################################### 1310 self.on_rotate_handler = None
1311
1312 - def set_filter(self, filter):
1313 self.filter = filter
1314
1315 - def set_dotcode(self, dotcode, filename='<stdin>'):
1316 p = subprocess.Popen( 1317 [self.filter, '-Txdot'], 1318 stdin=subprocess.PIPE, 1319 stdout=subprocess.PIPE, 1320 shell=False, 1321 universal_newlines=True 1322 ) 1323 xdotcode = p.communicate(dotcode)[0] 1324 try: 1325 self.set_xdotcode(xdotcode) 1326 except ParseError, ex: 1327 dialog = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, 1328 message_format=str(ex), 1329 buttons=gtk.BUTTONS_OK) 1330 dialog.set_title('Dot Viewer') 1331 dialog.run() 1332 dialog.destroy() 1333 return False 1334 else: 1335 return True
1336
1337 - def set_xdotcode(self, xdotcode):
1338 #print xdotcode 1339 parser = XDotParser(xdotcode) 1340 self.graph = parser.parse() 1341 self.zoom_image(self.zoom_ratio, center=True)
1342
1343 - def do_expose_event(self, event):
1344 cr = self.window.cairo_create() 1345 1346 # set a clip region for the expose event 1347 cr.rectangle( 1348 event.area.x, event.area.y, 1349 event.area.width, event.area.height 1350 ) 1351 cr.clip() 1352 1353 cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) 1354 cr.paint() 1355 1356 cr.save() 1357 rect = self.get_allocation() 1358 cr.translate(0.5*rect.width, 0.5*rect.height) 1359 cr.scale(self.zoom_ratio, self.zoom_ratio) 1360 cr.translate(-self.x, -self.y) 1361 1362 self.graph.draw(cr, highlight_items=self.highlight) 1363 cr.restore() 1364 1365 self.drag_action.draw(cr) 1366 1367 return False
1368
1369 - def get_current_pos(self):
1370 return self.x, self.y
1371
1372 - def set_current_pos(self, x, y):
1373 self.x = x 1374 self.y = y 1375 self.queue_draw()
1376
1377 - def set_highlight(self, items):
1378 if self.highlight != items: 1379 self.highlight = items 1380 self.queue_draw()
1381
1382 - def zoom_image(self, zoom_ratio, center=False, pos=None):
1383 if center: 1384 self.x = self.graph.width/2 1385 self.y = self.graph.height/2 1386 elif pos is not None: 1387 rect = self.get_allocation() 1388 x, y = pos 1389 x -= 0.5*rect.width 1390 y -= 0.5*rect.height 1391 self.x += x / self.zoom_ratio - x / zoom_ratio 1392 self.y += y / self.zoom_ratio - y / zoom_ratio 1393 self.zoom_ratio = zoom_ratio 1394 self.zoom_to_fit_on_resize = False 1395 self.queue_draw()
1396
1397 - def zoom_to_area(self, x1, y1, x2, y2):
1398 rect = self.get_allocation() 1399 width = abs(x1 - x2) 1400 height = abs(y1 - y2) 1401 self.zoom_ratio = min( 1402 float(rect.width)/float(width), 1403 float(rect.height)/float(height) 1404 ) 1405 self.zoom_to_fit_on_resize = False 1406 self.x = (x1 + x2) / 2 1407 self.y = (y1 + y2) / 2 1408 self.queue_draw()
1409
1410 - def zoom_to_fit(self):
1411 rect = self.get_allocation() 1412 rect.x += self.ZOOM_TO_FIT_MARGIN 1413 rect.y += self.ZOOM_TO_FIT_MARGIN 1414 rect.width -= 2 * self.ZOOM_TO_FIT_MARGIN 1415 rect.height -= 2 * self.ZOOM_TO_FIT_MARGIN 1416 zoom_ratio = min( 1417 float(rect.width)/float(self.graph.width), 1418 float(rect.height)/float(self.graph.height) 1419 ) 1420 self.zoom_image(zoom_ratio, center=True) 1421 self.zoom_to_fit_on_resize = True
1422 1423 ZOOM_INCREMENT = 1.25 1424 ZOOM_TO_FIT_MARGIN = 12 1425
1426 - def on_zoom_in(self, action):
1427 self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
1428
1429 - def on_zoom_out(self, action):
1430 self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
1431
1432 - def on_zoom_fit(self, action):
1433 self.zoom_to_fit()
1434
1435 - def on_zoom_100(self, action):
1436 self.zoom_image(1.0)
1437 1438 # ADDITIONS ##################################################### 1439 ## see set_on_rotate
1440 - def on_rotate(self, action):
1441 if self.on_rotate_handler: 1442 self.on_rotate_handler(action)
1443
1444 - def set_on_rotate(self, handler):
1445 self.on_rotate_handler = handler
1446 # END ADDITIONS ################################################# 1447 1448 POS_INCREMENT = 100 1449
1450 - def on_key_press_event(self, widget, event):
1451 if event.keyval == gtk.keysyms.Left: 1452 self.x -= self.POS_INCREMENT/self.zoom_ratio 1453 self.queue_draw() 1454 return True 1455 if event.keyval == gtk.keysyms.Right: 1456 self.x += self.POS_INCREMENT/self.zoom_ratio 1457 self.queue_draw() 1458 return True 1459 if event.keyval == gtk.keysyms.Up: 1460 self.y -= self.POS_INCREMENT/self.zoom_ratio 1461 self.queue_draw() 1462 return True 1463 if event.keyval == gtk.keysyms.Down: 1464 self.y += self.POS_INCREMENT/self.zoom_ratio 1465 self.queue_draw() 1466 return True 1467 if event.keyval == gtk.keysyms.Page_Up: 1468 self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT) 1469 self.queue_draw() 1470 return True 1471 if event.keyval == gtk.keysyms.Page_Down: 1472 self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT) 1473 self.queue_draw() 1474 return True 1475 if event.keyval == gtk.keysyms.Escape: 1476 self.drag_action.abort() 1477 self.drag_action = NullAction(self) 1478 return True 1479 return False
1480
1481 - def get_drag_action(self, event):
1482 state = event.state 1483 if event.button in (1, 2): # left or middle button 1484 if state & gtk.gdk.CONTROL_MASK: 1485 return ZoomAction 1486 elif state & gtk.gdk.SHIFT_MASK: 1487 return ZoomAreaAction 1488 else: 1489 return PanAction 1490 return NullAction
1491
1492 - def on_area_button_press(self, area, event):
1493 self.animation.stop() 1494 self.drag_action.abort() 1495 action_type = self.get_drag_action(event) 1496 self.drag_action = action_type(self) 1497 self.drag_action.on_button_press(event) 1498 self.presstime = time.time() 1499 self.pressx = event.x 1500 self.pressy = event.y 1501 return False
1502
1503 - def is_click(self, event, click_fuzz=4, click_timeout=1.0):
1504 assert event.type == gtk.gdk.BUTTON_RELEASE 1505 if self.presstime is None: 1506 # got a button release without seeing the press? 1507 return False 1508 # XXX instead of doing this complicated logic, shouldn't we listen 1509 # for gtk's clicked event instead? 1510 deltax = self.pressx - event.x 1511 deltay = self.pressy - event.y 1512 return (time.time() < self.presstime + click_timeout 1513 and math.hypot(deltax, deltay) < click_fuzz)
1514
1515 - def on_area_button_release(self, area, event):
1516 self.drag_action.on_button_release(event) 1517 self.drag_action = NullAction(self) 1518 if event.button == 1 and self.is_click(event): 1519 x, y = int(event.x), int(event.y) 1520 url = self.get_url(x, y) 1521 if url is not None: 1522 self.emit('clicked', unicode(url.url), event) 1523 else: 1524 jump = self.get_jump(x, y) 1525 if jump is not None: 1526 self.animate_to(jump.x, jump.y) 1527 1528 return True 1529 if event.button == 1 or event.button == 2: 1530 return True 1531 return False
1532
1533 - def on_area_scroll_event(self, area, event):
1534 if event.direction == gtk.gdk.SCROLL_UP: 1535 self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT, 1536 pos=(event.x, event.y)) 1537 return True 1538 if event.direction == gtk.gdk.SCROLL_DOWN: 1539 self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT, 1540 pos=(event.x, event.y)) 1541 return True 1542 return False
1543
1544 - def on_area_motion_notify(self, area, event):
1545 self.drag_action.on_motion_notify(event) 1546 return True
1547
1548 - def on_area_size_allocate(self, area, allocation):
1549 if self.zoom_to_fit_on_resize: 1550 self.zoom_to_fit()
1551
1552 - def animate_to(self, x, y):
1553 self.animation = ZoomToAnimation(self, x, y) 1554 self.animation.start()
1555
1556 - def window2graph(self, x, y):
1557 rect = self.get_allocation() 1558 x -= 0.5*rect.width 1559 y -= 0.5*rect.height 1560 x /= self.zoom_ratio 1561 y /= self.zoom_ratio 1562 x += self.x 1563 y += self.y 1564 return x, y
1565
1566 - def get_url(self, x, y):
1567 x, y = self.window2graph(x, y) 1568 return self.graph.get_url(x, y)
1569
1570 - def get_jump(self, x, y):
1571 x, y = self.window2graph(x, y) 1572 return self.graph.get_jump(x, y)
1573 1574
1575 -class DotWindow(gtk.Window):
1576 1577 ui = ''' 1578 <ui> 1579 <toolbar name="ToolBar"> 1580 <toolitem action="ZoomIn"/> 1581 <toolitem action="ZoomOut"/> 1582 <toolitem action="ZoomFit"/> 1583 <toolitem action="Zoom100"/> 1584 </toolbar> 1585 </ui> 1586 ''' 1587
1588 - def __init__(self, default_size):
1589 gtk.Window.__init__(self) 1590 1591 self.graph = Graph() 1592 1593 window = self 1594 1595 window.set_title('Dot Viewer') 1596 window.set_default_size(*default_size) 1597 vbox = gtk.VBox() 1598 window.add(vbox) 1599 1600 self.widget = DotWidget() 1601 1602 # Create a UIManager instance 1603 uimanager = self.uimanager = gtk.UIManager() 1604 1605 # Add the accelerator group to the toplevel window 1606 accelgroup = uimanager.get_accel_group() 1607 window.add_accel_group(accelgroup) 1608 1609 # Create an ActionGroup 1610 actiongroup = gtk.ActionGroup('Actions') 1611 self.actiongroup = actiongroup 1612 1613 # Create actions 1614 actiongroup.add_actions(( 1615 ('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.widget.on_zoom_in), 1616 ('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.widget.on_zoom_out), 1617 ('ZoomFit', gtk.STOCK_ZOOM_FIT, None, None, None, self.widget.on_zoom_fit), 1618 ('Zoom100', gtk.STOCK_ZOOM_100, None, None, None, self.widget.on_zoom_100), 1619 )) 1620 1621 # Add the actiongroup to the uimanager 1622 uimanager.insert_action_group(actiongroup, 0) 1623 1624 # Add a UI descrption 1625 uimanager.add_ui_from_string(self.ui) 1626 1627 # Create a Toolbar 1628 toolbar = uimanager.get_widget('/ToolBar') 1629 vbox.pack_start(toolbar, False) 1630 1631 vbox.pack_start(self.widget) 1632 1633 self.set_focus(self.widget) 1634 1635 self.show_all()
1636
1637 - def set_filter(self, filter):
1638 self.widget.set_filter(filter)
1639
1640 - def set_dotcode(self, dotcode, filename='<stdin>'):
1641 if self.widget.set_dotcode(dotcode, filename): 1642 self.set_title(os.path.basename(filename) + ' - Dot Viewer') 1643 self.widget.zoom_to_fit()
1644
1645 - def set_xdotcode(self, xdotcode, filename='<stdin>'):
1646 if self.widget.set_xdotcode(xdotcode): 1647 self.set_title(os.path.basename(filename) + ' - Dot Viewer') 1648 self.widget.zoom_to_fit()
1649
1650 - def open_file(self, filename):
1651 try: 1652 fp = file(filename, 'rt') 1653 self.set_dotcode(fp.read(), filename) 1654 fp.close() 1655 except IOError, ex: 1656 pass
1657 1658
1659 -def main():
1660 import optparse 1661 1662 parser = optparse.OptionParser( 1663 usage='\n\t%prog [file]', 1664 version='%%prog %s' % __version__) 1665 parser.add_option( 1666 '-f', '--filter', 1667 type='choice', choices=('dot', 'neato', 'twopi', 'circo', 'fdp'), 1668 dest='filter', default='dot', 1669 help='graphviz filter: dot, neato, twopi, circo, or fdp [default: %default]') 1670 parser.add_option('--raw', 1671 dest='raw', action="store_true",) 1672 1673 (options, args) = parser.parse_args(sys.argv[1:]) 1674 if len(args) > 1: 1675 parser.error('incorrect number of arguments') 1676 1677 win = DotWindow((800, 600)) 1678 win.connect('destroy', gtk.main_quit) 1679 win.set_filter(options.filter) 1680 if len(args) >= 1: 1681 if args[0] == '-': 1682 win.set_dotcode(sys.stdin.read()) 1683 else: 1684 if options.raw: 1685 win.set_dotcode(args[0]) 1686 else: 1687 win.open_file(args[0]) 1688 gtk.main()
1689 1690 1691 if __name__ == '__main__': 1692 main() 1693