Browse code

Fix clicks.

Jose.R.Fonseca authored on 22/12/2007 11:46:09
Showing 1 changed files

1 1
new file mode 100644
... ...
@@ -0,0 +1,686 @@
1
+#!/usr/bin/env python
2
+'''Visualize dot graphs via the xdot format.'''
3
+
4
+__author__ = "Jose Fonseca"
5
+
6
+__version__ = "0.2"
7
+
8
+
9
+import sys
10
+import subprocess
11
+import math
12
+
13
+import gtk
14
+import gtk.gdk
15
+import gtk.keysyms
16
+import cairo
17
+import pango
18
+import pangocairo
19
+
20
+import pydot
21
+
22
+
23
+# See http://www.graphviz.org/pub/scm/graphviz-cairo/plugin/cairo/gvrender_cairo.c
24
+
25
+# For pygtk inspiration and guidance see:
26
+# - http://mirageiv.berlios.de/
27
+# - http://comix.sourceforge.net/
28
+
29
+
30
+class Pen:
31
+	"""Store pen attributes."""
32
+
33
+	def __init__(self):
34
+		self.color = (0.0, 0.0, 0.0, 1.0)
35
+		self.fillcolor = (0.0, 0.0, 0.0, 1.0)
36
+		self.linewidth = 1.0
37
+		self.fontsize = 14.0
38
+		self.fontname = "Times-Roman"
39
+
40
+	def copy(self):
41
+		"""Create a copy of this pen."""
42
+		pen = Pen()
43
+		pen.__dict__ = self.__dict__.copy()
44
+		return pen
45
+
46
+
47
+class Shape:
48
+	"""Abstract base class for all the drawing shapes."""
49
+
50
+	def __init__(self):
51
+		pass
52
+
53
+	def draw(self, cr):
54
+		"""Draw this shape with the given cairo context"""
55
+		raise NotImplementedError
56
+
57
+	def boundingbox(self):
58
+		"""Get the bounding box of this shape."""
59
+		raise NotImplementedError
60
+
61
+
62
+class TextShape(Shape):
63
+	
64
+	#fontmap = pangocairo.CairoFontMap()
65
+	#fontmap.set_resolution(72)
66
+	#context = fontmap.create_context()
67
+
68
+	LEFT, CENTER, RIGHT = -1, 0, 1
69
+
70
+	def __init__(self, pen, x, y, j, w, t):
71
+		Shape.__init__(self)
72
+		self.pen = pen.copy()
73
+		self.x = x
74
+		self.y = y
75
+		self.j = j
76
+		self.w = w
77
+		self.t = t
78
+
79
+	def draw(self, cr):
80
+
81
+		try:
82
+			layout = self.layout
83
+		except AttributeError:
84
+			layout = cr.create_layout()
85
+			
86
+			# set font options
87
+			# see http://lists.freedesktop.org/archives/cairo/2007-February/009688.html
88
+			context = layout.get_context()
89
+			fo = cairo.FontOptions()
90
+			fo.set_antialias(cairo.ANTIALIAS_DEFAULT)
91
+			fo.set_hint_style(cairo.HINT_STYLE_NONE)
92
+			fo.set_hint_metrics(cairo.HINT_METRICS_OFF)
93
+			pangocairo.context_set_font_options(context, fo)
94
+			
95
+			# set font
96
+			font = pango.FontDescription()
97
+			font.set_family(self.pen.fontname)
98
+			font.set_absolute_size(self.pen.fontsize*pango.SCALE)
99
+			layout.set_font_description(font)
100
+			
101
+			# set text
102
+			layout.set_text(self.t)
103
+			
104
+			# cache it
105
+			self.layout = layout
106
+		else:
107
+			cr.update_layout(layout)
108
+
109
+		width, height = layout.get_size()
110
+		width = float(width)/pango.SCALE
111
+		height = float(height)/pango.SCALE
112
+
113
+		cr.move_to(self.x - self.w/2, self.y)
114
+
115
+		if self.j == self.LEFT:
116
+			x = self.x
117
+		elif self.j == self.CENTER:
118
+			x = self.x - 0.5*width
119
+		elif self.j == self.RIGHT:
120
+			x = self.x - width
121
+		else:
122
+			assert 0
123
+		
124
+		y = self.y - height
125
+		
126
+		cr.move_to(x, y)
127
+
128
+		cr.set_source_rgba(*self.pen.color)
129
+		cr.show_layout(layout)
130
+
131
+
132
+class EllipseShape(Shape):
133
+
134
+	def __init__(self, pen, x0, y0, w, h, filled=False):
135
+		Shape.__init__(self)
136
+		self.pen = pen.copy()
137
+		self.x0 = x0
138
+		self.y0 = y0
139
+		self.w = w
140
+		self.h = h
141
+		self.filled = filled
142
+
143
+	def draw(self, cr):
144
+		cr.save()
145
+		cr.translate(self.x0, self.y0)
146
+		cr.scale(self.w, self.h)
147
+		cr.arc(0.0, 0.0, 1.0, 0, 2.0*math.pi)
148
+		cr.restore()
149
+		if self.filled:
150
+			cr.set_source_rgba(*self.pen.fillcolor)
151
+			cr.fill()
152
+		else:
153
+			cr.set_line_width(self.pen.linewidth)
154
+			cr.set_source_rgba(*self.pen.color)
155
+			cr.stroke()
156
+
157
+
158
+class PolygonShape(Shape):
159
+
160
+	def __init__(self, pen, points, filled=False):
161
+		Shape.__init__(self)
162
+		self.pen = pen.copy()
163
+		self.points = points
164
+		self.filled = filled
165
+
166
+	def draw(self, cr):
167
+		x0, y0 = self.points[-1]
168
+		cr.move_to(x0, y0)
169
+		for x, y in self.points:
170
+			cr.line_to(x, y)
171
+		cr.close_path()
172
+		if self.filled:
173
+			cr.set_source_rgba(*self.pen.fillcolor)
174
+			cr.fill_preserve()
175
+			cr.fill()
176
+		else:
177
+			cr.set_line_width(self.pen.linewidth)
178
+			cr.set_source_rgba(*self.pen.color)
179
+			cr.stroke()
180
+
181
+
182
+class BezierShape(Shape):
183
+
184
+	def __init__(self, pen, points):
185
+		Shape.__init__(self)
186
+		self.pen = pen.copy()
187
+		self.points = points
188
+
189
+	def draw(self, cr):
190
+		x0, y0 = self.points[0]
191
+		cr.move_to(x0, y0)
192
+		for i in xrange(1, len(self.points), 3):
193
+			x1, y1 = self.points[i]
194
+			x2, y2 = self.points[i + 1]
195
+			x3, y3 = self.points[i + 2]
196
+			cr.curve_to(x1, y1, x2, y2, x3, y3)
197
+		cr.set_line_width(self.pen.linewidth)
198
+		cr.set_source_rgba(*self.pen.color)
199
+		cr.stroke()
200
+
201
+
202
+class CompoundShape(Shape):
203
+
204
+	def __init__(self, shapes):
205
+		Shape.__init__(self)
206
+		self.shapes = shapes
207
+
208
+	def draw(self, cr):
209
+		for shape in self.shapes:
210
+			shape.draw(cr)
211
+
212
+
213
+class XDotAttrParser:
214
+	"""Parser for xdot drawing attributes.
215
+	See also:
216
+	- http://www.graphviz.org/doc/info/output.html#d:xdot
217
+	"""
218
+
219
+	def __init__(self, parser, buf):
220
+		self.parser = parser
221
+		self.buf = self.unescape(buf)
222
+		self.pos = 0
223
+
224
+	def __nonzero__(self):
225
+		return self.pos < len(self.buf)
226
+
227
+	def unescape(self, buf):
228
+		buf = buf.replace('\\"', '"')
229
+		buf = buf.replace('\\n', '\n')
230
+		return buf
231
+
232
+	def read_code(self):
233
+		pos = self.buf.find(" ", self.pos)
234
+		res = self.buf[self.pos:pos]
235
+		self.pos = pos + 1
236
+		while self.pos < len(self.buf) and self.buf[self.pos].isspace():
237
+			self.pos += 1
238
+		return res
239
+
240
+	def read_number(self):
241
+		return int(self.read_code())
242
+
243
+	def read_float(self):
244
+		return float(self.read_code())
245
+
246
+	def read_point(self):
247
+		x = self.read_number()
248
+		y = self.read_number()
249
+		return self.transform(x, y)
250
+
251
+	def read_text(self):
252
+		num = self.read_number()
253
+		pos = self.buf.find("-", self.pos) + 1
254
+		self.pos = pos + num
255
+		res = self.buf[pos:self.pos]
256
+		while self.pos < len(self.buf) and self.buf[self.pos].isspace():
257
+			self.pos += 1
258
+		return res
259
+
260
+	def read_polygon(self):
261
+		n = self.read_number()
262
+		p = []
263
+		for i in range(n):
264
+			x, y = self.read_point()
265
+			p.append((x, y))
266
+		return p
267
+
268
+	def read_color(self):
269
+		# See http://www.graphviz.org/doc/info/attrs.html#k:color
270
+		c = self.read_text()
271
+		c1 = c[:1]
272
+		if c1 == '#':
273
+			hex2float = lambda h: float(int(h, 16)/255.0)
274
+			r = hex2float(c[1:3])
275
+			g = hex2float(c[3:5])
276
+			b = hex2float(c[5:7])
277
+			try:
278
+				a = hex2float(c[7:9])
279
+			except (IndexError, ValueError):
280
+				a = 1.0
281
+			return r, g, b, a
282
+		elif c1.isdigit():
283
+			h, s, v = map(float, c[1:].split(","))
284
+			raise NotImplementedError
285
+		else:
286
+			color = gtk.gdk.color_parse(c)
287
+			s = 1.0/65535.0
288
+			r = color.red*s
289
+			g = color.green*s
290
+			b = color.blue*s
291
+			a = 1.0
292
+			return r, g, b, a
293
+
294
+	def parse(self):
295
+		shapes = []
296
+		pen = Pen()
297
+		s = self
298
+
299
+		while s:
300
+			op = s.read_code()
301
+			if op == "c":
302
+				pen.color = s.read_color()
303
+			elif op == "C":
304
+				pen.fillcolor = s.read_color()
305
+			elif op == "S":
306
+				s.read_text()
307
+			elif op == "F":
308
+				pen.fontsize = s.read_float()
309
+				pen.fontname = s.read_text()
310
+			elif op == "T":
311
+				x, y = s.read_point()
312
+				j = s.read_number()
313
+				w = s.read_number()
314
+				t = s.read_text()
315
+				shapes.append(TextShape(pen, x, y, j, w, t))
316
+			elif op == "E":
317
+				x0, y0 = s.read_point()
318
+				w = s.read_number()
319
+				h = s.read_number()
320
+				shapes.append(EllipseShape(pen, x0, y0, w, h, filled=True))
321
+			elif op == "e":
322
+				x0, y0 = s.read_point()
323
+				w = s.read_number()
324
+				h = s.read_number()
325
+				shapes.append(EllipseShape(pen, x0, y0, w, h))
326
+			elif op == "B":
327
+				p = self.read_polygon()
328
+				shapes.append(BezierShape(pen, p))
329
+			elif op == "P":
330
+				p = self.read_polygon()
331
+				shapes.append(PolygonShape(pen, p, filled=True))
332
+			elif op == "p":
333
+				p = self.read_polygon()
334
+				shapes.append(PolygonShape(pen, p))
335
+			else:
336
+				sys.stderr.write("unknown xdot opcode '%s'\n" % op)
337
+				break
338
+		return CompoundShape(shapes)
339
+
340
+	def transform(self, x, y):
341
+		return self.parser.transform(x, y)
342
+
343
+
344
+class Hyperlink:
345
+
346
+	def __init__(self, url, x, y, w, h):
347
+		self.url = url
348
+		self.x1 = x - w/2
349
+		self.y1 = y - h/2
350
+		self.x2 = x + w/2
351
+		self.y2 = y + h/2
352
+
353
+	def hit(self, x, y):
354
+		#print (x, y), (self.x1, self.y1), "-", (self.x2, self.y2)
355
+		return self.x1 <= x and x <= self.x2 and self.y1 <= y and y <= self.y2
356
+
357
+
358
+class DotWindow(gtk.Window):
359
+
360
+	# TODO: Make a seperate, reusable widget
361
+
362
+	ui = '''
363
+	<ui>
364
+		<toolbar name="ToolBar">
365
+			<toolitem action="ZoomIn"/>
366
+			<toolitem action="ZoomOut"/>
367
+			<toolitem action="ZoomFit"/>
368
+			<toolitem action="Zoom100"/>
369
+		</toolbar>
370
+	</ui>
371
+	'''
372
+
373
+	def __init__(self):
374
+		gtk.Window.__init__(self)
375
+
376
+		self.graph = None
377
+		self.width = 1
378
+		self.height = 1
379
+		self.shapes = []
380
+		self.hyperlinks = []
381
+
382
+		window = self
383
+
384
+		window.set_title('Dot')
385
+		window.set_default_size(512, 512)
386
+		vbox = gtk.VBox()
387
+		window.add(vbox)
388
+
389
+		# Create a UIManager instance
390
+		uimanager = self.uimanager = gtk.UIManager()
391
+
392
+		# Add the accelerator group to the toplevel window
393
+		accelgroup = uimanager.get_accel_group()
394
+		window.add_accel_group(accelgroup)
395
+
396
+		# Create an ActionGroup
397
+		actiongroup = gtk.ActionGroup('Actions')
398
+		self.actiongroup = actiongroup
399
+
400
+		# Create actions
401
+		actiongroup.add_actions((
402
+			('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.on_zoom_in),
403
+			('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.on_zoom_out),
404
+			('ZoomFit', gtk.STOCK_ZOOM_FIT, None, None, None, self.on_zoom_fit),
405
+			('Zoom100', gtk.STOCK_ZOOM_100, None, None, None, self.on_zoom_100),
406
+		))
407
+
408
+		# Add the actiongroup to the uimanager
409
+		uimanager.insert_action_group(actiongroup, 0)
410
+
411
+		# Add a UI description
412
+		uimanager.add_ui_from_string(self.ui)
413
+
414
+		# Create a Toolbar
415
+		toolbar = uimanager.get_widget('/ToolBar')
416
+		vbox.pack_start(toolbar, False)
417
+
418
+		# TODO: Use a custom widget instead of Layout like in the scrollable.py example?
419
+		#scrolled_window = self.scrolled_window = gtk.ScrolledWindow()
420
+		#scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
421
+		#vbox.pack_start(scrolled_window)
422
+
423
+		self.area = gtk.DrawingArea()
424
+		self.area.connect("expose_event", self.on_expose)
425
+		#scrolled_window.add(self.area)
426
+		vbox.pack_start(self.area)
427
+
428
+		self.area.set_flags(gtk.CAN_FOCUS)
429
+		self.set_focus(self.area)
430
+
431
+		self.area.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
432
+		self.area.connect("button-press-event", self.on_area_button_press)
433
+		self.area.connect("button-release-event", self.on_area_button_release)
434
+		self.area.add_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
435
+		self.area.connect("motion-notify-event", self.on_area_motion_notify)
436
+		self.area.connect("scroll-event", self.on_area_scroll_event)
437
+
438
+		self.connect('key-press-event', self.on_key_press_event)
439
+
440
+		self.x, self.y = 0.0, 0.0
441
+		self.zoom_ratio = 1.0
442
+		self.pixbuf = None
443
+
444
+		self.show_all()
445
+
446
+	def set_dotcode(self, dotcode):
447
+		p = subprocess.Popen(
448
+			['dot', '-Txdot'],
449
+			stdin=subprocess.PIPE,
450
+			stdout=subprocess.PIPE,
451
+			shell=False,
452
+			universal_newlines=True
453
+		)
454
+		xdotcode = p.communicate(dotcode)[0]
455
+		#sys.stdout.write(xdotcode)
456
+		self.parse(xdotcode)
457
+		self.zoom_image(self.zoom_ratio, center=True)
458
+
459
+	def parse(self, xdotcode):
460
+		self.graph = pydot.graph_from_dot_data(xdotcode)
461
+
462
+		bb = self.graph.get_bb()
463
+		if bb is None:
464
+			return
465
+
466
+		xmin, ymin, xmax, ymax = map(int, bb.split(","))
467
+
468
+		self.xoffset = -xmin
469
+		self.yoffset = -ymax
470
+		self.xscale = 1.0
471
+		self.yscale = -1.0
472
+		self.width = xmax - xmin
473
+		self.height = ymax - ymin
474
+
475
+		self.shapes = []
476
+		self.hyperlinks = []
477
+
478
+		for node in self.graph.get_node_list():
479
+			for attr in ("_draw_", "_ldraw_"):
480
+				if hasattr(node, attr):
481
+					p = XDotAttrParser(self, getattr(node, attr))
482
+					self.shapes.append(p.parse())
483
+			if node.URL is not None:
484
+				x, y = map(float, node.pos.split(","))
485
+				w = float(node.width)*72
486
+				h = float(node.height)*72
487
+				self.hyperlinks.append(Hyperlink(node.URL, x, y, w, h))
488
+		for edge in self.graph.get_edge_list():
489
+			for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
490
+				if hasattr(edge, attr):
491
+					p = XDotAttrParser(self, getattr(edge, attr))
492
+					self.shapes.append(p.parse())
493
+
494
+	def transform(self, x, y):
495
+		# XXX: this is not the right place for this code
496
+		x = (x + self.xoffset)*self.xscale
497
+		y = (y + self.yoffset)*self.yscale
498
+		return x, y
499
+
500
+	def on_expose(self, area, event):
501
+		cr = area.window.cairo_create()
502
+
503
+		# set a clip region for the expose event
504
+		cr.rectangle(
505
+			event.area.x, event.area.y,
506
+			event.area.width, event.area.height
507
+		)
508
+		cr.clip()
509
+
510
+		cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
511
+		cr.paint()
512
+
513
+		rect = self.area.get_allocation()
514
+		cr.translate(0.5*rect.width, 0.5*rect.height)
515
+		cr.scale(self.zoom_ratio, self.zoom_ratio)
516
+		cr.translate(-self.x, -self.y)
517
+
518
+		# FIXME: scale from points to pixels
519
+
520
+		cr.set_source_rgba(0.0, 0.0, 0.0, 1.0)
521
+
522
+		cr.set_line_cap(cairo.LINE_CAP_BUTT)
523
+		cr.set_line_join(cairo.LINE_JOIN_MITER)
524
+		
525
+		for shape in self.shapes:
526
+			shape.draw(cr)
527
+
528
+		return False
529
+
530
+	def get_current_pos(self):
531
+		return self.x, self.y
532
+
533
+	def set_current_pos(self, x, y):
534
+		self.x = x
535
+		self.y = y
536
+		self.area.queue_draw()
537
+
538
+	def zoom_image(self, zoom_ratio, center=False):
539
+		if center:
540
+			self.x = self.width/2
541
+			self.y = self.height/2
542
+		self.zoom_ratio = zoom_ratio
543
+		self.area.queue_draw()
544
+
545
+	ZOOM_INCREMENT = 1.25
546
+
547
+	def on_zoom_in(self, action):
548
+		self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
549
+
550
+	def on_zoom_out(self, action):
551
+		self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
552
+
553
+	def on_zoom_fit(self, action):
554
+		rect = self.area.get_allocation()
555
+		zoom_ratio = min(
556
+			float(rect.width)/float(self.width),
557
+			float(rect.height)/float(self.height)
558
+		)
559
+		self.zoom_image(zoom_ratio, center=True)
560
+
561
+	def on_zoom_100(self, action):
562
+		self.zoom_image(1.0)
563
+
564
+	POS_INCREMENT = 100
565
+
566
+	def on_key_press_event(self, widget, event):
567
+		if event.keyval == gtk.keysyms.Left:
568
+			self.x -= self.POS_INCREMENT/self.zoom_ratio
569
+			self.area.queue_draw()
570
+			return True
571
+		if event.keyval == gtk.keysyms.Right:
572
+			self.x += self.POS_INCREMENT/self.zoom_ratio
573
+			self.area.queue_draw()
574
+			return True
575
+		if event.keyval == gtk.keysyms.Up:
576
+			self.y -= self.POS_INCREMENT/self.zoom_ratio
577
+			self.area.queue_draw()
578
+			return True
579
+		if event.keyval == gtk.keysyms.Down:
580
+			self.y += self.POS_INCREMENT/self.zoom_ratio
581
+			self.area.queue_draw()
582
+			return True
583
+		if event.keyval == gtk.keysyms.Page_Up:
584
+			self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
585
+			self.area.queue_draw()
586
+			return True
587
+		if event.keyval == gtk.keysyms.Page_Down:
588
+			self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
589
+			self.area.queue_draw()
590
+			return True
591
+		return False
592
+
593
+	def on_area_button_press(self, area, event):
594
+		if event.button == 2 or event.button == 1:
595
+			area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
596
+			self.prevmousex = event.x
597
+			self.prevmousey = event.y
598
+
599
+		if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk.BUTTON_RELEASE):
600
+			return False
601
+		x, y = int(event.x), int(event.y)
602
+		url = self.get_url(x, y)
603
+		if url is not None:
604
+			return self.on_url_clicked(url, event)
605
+
606
+		return False
607
+
608
+	def on_area_button_release(self, area, event):
609
+		if event.button == 2 or event.button == 1:
610
+			area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
611
+			self.prevmousex = None
612
+			self.prevmousey = None
613
+			return True
614
+		return False
615
+
616
+	def on_area_scroll_event(self, area, event):
617
+		if event.direction == gtk.gdk.SCROLL_UP:
618
+			self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
619
+			return True
620
+		if event.direction == gtk.gdk.SCROLL_DOWN:
621
+			self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
622
+			return True
623
+		return False
624
+
625
+	def on_area_motion_notify(self, area, event):
626
+		x, y = int(event.x), int(event.y)
627
+		state = event.state
628
+
629
+		if state & gtk.gdk.BUTTON2_MASK or state & gtk.gdk.BUTTON1_MASK:
630
+			# pan the image
631
+			self.x += (self.prevmousex - x)/self.zoom_ratio
632
+			self.y += (self.prevmousey - y)/self.zoom_ratio
633
+			self.area.queue_draw()
634
+			self.prevmousex = x
635
+			self.prevmousey = y
636
+		else:
637
+			# set cursor
638
+			if self.get_url(x, y) is not None:
639
+				area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
640
+			else:
641
+				area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
642
+
643
+		return True
644
+
645
+	def get_url(self, x, y):
646
+		rect = self.area.get_allocation()
647
+		x -= 0.5*rect.width
648
+		y = 0.5*rect.height - y
649
+		x /= self.zoom_ratio
650
+		y /= self.zoom_ratio
651
+		x += self.x
652
+		y += self.y
653
+
654
+		for hyperlink in self.hyperlinks:
655
+			if hyperlink.hit(x, y):
656
+				return hyperlink.url
657
+		return None
658
+
659
+	def on_url_clicked(self, url, event):
660
+		return False
661
+
662
+
663
+def main():
664
+	import optparse
665
+	
666
+	parser = optparse.OptionParser(
667
+		usage="\n\t%prog [file]",
668
+		version="%%prog %s" % __version__)
669
+	
670
+	(options, args) = parser.parse_args(sys.argv[1:])
671
+	
672
+	if len(args) == 0:
673
+		fp = sys.stdin
674
+	elif len(args) == 1:
675
+		fp = file(args[0], 'rt')
676
+	else:
677
+		parser.error('incorrect number of arguments')
678
+	
679
+	win = DotWindow()
680
+	win.set_dotcode(fp.read())
681
+	win.connect('destroy', gtk.main_quit)
682
+	gtk.main()
683
+
684
+
685
+if __name__ == '__main__':
686
+	main()