1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
44
45
46
47
48
49
51 """Store pen attributes."""
52
54
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
63 """Create a copy of this pen."""
64 pen = Pen()
65 pen.__dict__ = self.__dict__.copy()
66 return pen
67
69 pen = self.copy()
70 pen.color = (1, 0, 0, 1)
71 pen.fillcolor = (1, .8, .8, 1)
72 return pen
73
74
76 """Abstract base class for all the drawing shapes."""
77
80
81 - def draw(self, cr, highlight=False):
82 """Draw this shape with the given cairo context"""
83 raise NotImplementedError
84
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
97
98
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
119
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
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
134 layout.set_text(self.t)
135
136
137 self.layout = layout
138 else:
139 cr.update_layout(layout)
140
141 descent = 2
142
143 width, height = layout.get_size()
144 width = float(width)/pango.SCALE
145 height = float(height)/pango.SCALE
146
147
148
149 if width > self.w:
150 f = self.w / width
151 width = self.w
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:
177
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
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
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
246
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
265
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
287
291
292 - def draw(self, cr, highlight=False):
293 for shape in self.shapes:
294 shape.draw(cr, highlight=highlight)
295
296
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
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
319 """Base class for graph nodes and edges."""
320
323
326
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
347 return self.x1 <= x and x <= self.x2 and self.y1 <= y and y <= self.y2
348
350 if self.url is None:
351 return None
352
353 if self.is_inside(x, y):
354 return Url(self, self.url)
355 return None
356
358 if self.is_inside(x, y):
359 return Jump(self, self.x, self.y)
360 return None
361
362
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
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
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
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
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
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
433 """Parser for xdot drawing attributes.
434 See also:
435 - http://www.graphviz.org/doc/info/output.html#d:xdot
436 """
437
439 self.parser = parser
440 self.buf = self.unescape(buf)
441 self.pos = 0
442
444 return self.pos < len(self.buf)
445
447 buf = buf.replace('\\"', '"')
448 buf = buf.replace('\\n', '\n')
449 return buf
450
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
461
464
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
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
488
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
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
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, )
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
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
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
582
583
584 EOF = -1
585 SKIP = -2
586
587
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
597 return ':'.join([str(part) for part in (self.filename, self.line, self.col, self.msg) if part != None])
598
599
601 """Stateless scanner."""
602
603
604 tokens = []
605 symbols = {}
606 literals = {}
607 ignorecase = False
608
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
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
644
645
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
659 buf = fp.read()
660 pos = 0
661 else:
662
663 if length:
664
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
684 while True:
685
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
710
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
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
729
731 self.lexer = lexer
732 self.lookahead = self.lexer.next()
733
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
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
776
777
778 tokens = [
779
780 (SKIP,
781 r'[ \t\f\r\n\v]+|'
782 r'//[^\r\n]*|'
783 r'/\*.*?\*/|'
784 r'#[^\r\n]*',
785 False),
786
787
788 (ID, r'[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*', True),
789
790
791 (ID, r'-?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)', False),
792
793
794 (STR_ID, r'"[^"\\]*(?:\\.[^"\\]*)*"', False),
795
796
797 (HTML_ID, r'<[^<>]*(?:<[^<>]*>[^<>]*)*>', False),
798
799
800 (EDGE_OP, r'-[>-]', False),
801 ]
802
803
804 symbols = {
805 '[': LSQUARE,
806 ']': RSQUARE,
807 '{': LCURLY,
808 '}': RCURLY,
809 ',': COMMA,
810 ':': COLON,
811 ';': SEMI,
812 '=': EQUAL,
813 '+': PLUS,
814 }
815
816
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
830
831 scanner = DotScanner()
832
833 - def filter(self, type, text):
834
835 if type == STR_ID:
836 text = text[1:-1]
837
838
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
858
860 Parser.__init__(self, lexer)
861 self.graph_attrs = {}
862 self.node_attrs = {}
863 self.edge_attrs = {}
864
868
877
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
924
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
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
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
960 return node_id
961
963 self.match(ID)
964 id = self.lookahead.text
965 self.consume()
966 return id
967
970
973
976
977
979
987
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
1004
1005 self.width = xmax - xmin
1006 self.height = ymax - ymin
1007
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
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
1049
1051 x, y = pos.split(",")
1052 return self.transform(float(x), float(y))
1053
1055 points = []
1056 for entry in pos.split(' '):
1057 fields = entry.split(',')
1058 try:
1059 x, y = fields
1060 except ValueError:
1061
1062 continue
1063 else:
1064 points.append(self.transform(float(x), float(y)))
1065 return points
1066
1072
1073
1075
1076 step = 0.03
1077
1079 self.dot_widget = dot_widget
1080 self.timeout_id = None
1081
1083 self.timeout_id = gobject.timeout_add(int(self.step * 1000), self.tick)
1084
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
1093
1094
1102
1103
1105
1106 duration = 0.6
1107
1111
1113 t = (time.time() - self.started) / self.duration
1114 self.animate(max(0, min(t, 1)))
1115 return (t < 1)
1116
1119
1120
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
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
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
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
1165
1167 self.dot_widget = dot_widget
1168
1173
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
1185
1186 - def draw(self, cr):
1188
1191
1192 - def drag(self, deltax, deltay):
1194
1197
1200
1201
1203
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
1218
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
1228 self.dot_widget.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
1229
1230 abort = stop
1231
1232
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
1241 self.dot_widget.queue_draw()
1242
1243
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
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
1272 self.dot_widget.queue_draw()
1273
1274
1573
1574
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
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
1603 uimanager = self.uimanager = gtk.UIManager()
1604
1605
1606 accelgroup = uimanager.get_accel_group()
1607 window.add_accel_group(accelgroup)
1608
1609
1610 actiongroup = gtk.ActionGroup('Actions')
1611 self.actiongroup = actiongroup
1612
1613
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
1622 uimanager.insert_action_group(actiongroup, 0)
1623
1624
1625 uimanager.add_ui_from_string(self.ui)
1626
1627
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
1639
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
1646 if self.widget.set_xdotcode(xdotcode):
1647 self.set_title(os.path.basename(filename) + ' - Dot Viewer')
1648 self.widget.zoom_to_fit()
1649
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
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