Browse code

Convert tabs to spaces.

Jose.R.Fonseca authored on 13/07/2008 01:24:03
Showing 2 changed files

... ...
@@ -25,35 +25,35 @@ import xdot
25 25
 
26 26
 class MyDotWindow(xdot.DotWindow):
27 27
 
28
-	def __init__(self):
29
-		xdot.DotWindow.__init__(self)
30
-		self.widget.connect('clicked', self.on_url_clicked)
28
+    def __init__(self):
29
+        xdot.DotWindow.__init__(self)
30
+        self.widget.connect('clicked', self.on_url_clicked)
31 31
 
32
-	def on_url_clicked(self, widget, url, event):
33
-		dialog = gtk.MessageDialog(
34
-				parent = self, 
35
-				buttons = gtk.BUTTONS_OK,
36
-				message_format="%s clicked" % url)
37
-		dialog.connect('response', lambda dialog, response: dialog.destroy())
38
-		dialog.run()
39
-		return True
32
+    def on_url_clicked(self, widget, url, event):
33
+        dialog = gtk.MessageDialog(
34
+                parent = self, 
35
+                buttons = gtk.BUTTONS_OK,
36
+                message_format="%s clicked" % url)
37
+        dialog.connect('response', lambda dialog, response: dialog.destroy())
38
+        dialog.run()
39
+        return True
40 40
 
41 41
 
42 42
 dotcode = """
43 43
 digraph G {
44 44
   Hello [URL="http://en.wikipedia.org/wiki/Hello"]
45 45
   World [URL="http://en.wikipedia.org/wiki/World"]
46
-	Hello -> World
46
+    Hello -> World
47 47
 }
48 48
 """
49 49
 
50 50
 
51 51
 def main():
52
-	window = MyDotWindow()
53
-	window.set_dotcode(dotcode)
54
-	window.connect('destroy', gtk.main_quit)
55
-	gtk.main()
52
+    window = MyDotWindow()
53
+    window.set_dotcode(dotcode)
54
+    window.connect('destroy', gtk.main_quit)
55
+    gtk.main()
56 56
 
57 57
 
58 58
 if __name__ == '__main__':
59
-	main()
59
+    main()
... ...
@@ -46,802 +46,802 @@ import pydot
46 46
 
47 47
 
48 48
 class Pen:
49
-	"""Store pen attributes."""
49
+    """Store pen attributes."""
50 50
 
51
-	def __init__(self):
52
-		# set default attributes
53
-		self.color = (0.0, 0.0, 0.0, 1.0)
54
-		self.fillcolor = (0.0, 0.0, 0.0, 1.0)
55
-		self.linewidth = 1.0
56
-		self.fontsize = 14.0
57
-		self.fontname = "Times-Roman"
51
+    def __init__(self):
52
+        # set default attributes
53
+        self.color = (0.0, 0.0, 0.0, 1.0)
54
+        self.fillcolor = (0.0, 0.0, 0.0, 1.0)
55
+        self.linewidth = 1.0
56
+        self.fontsize = 14.0
57
+        self.fontname = "Times-Roman"
58 58
 
59
-	def copy(self):
60
-		"""Create a copy of this pen."""
61
-		pen = Pen()
62
-		pen.__dict__ = self.__dict__.copy()
63
-		return pen
59
+    def copy(self):
60
+        """Create a copy of this pen."""
61
+        pen = Pen()
62
+        pen.__dict__ = self.__dict__.copy()
63
+        return pen
64 64
 
65 65
 
66 66
 class Shape:
67
-	"""Abstract base class for all the drawing shapes."""
67
+    """Abstract base class for all the drawing shapes."""
68 68
 
69
-	def __init__(self):
70
-		pass
69
+    def __init__(self):
70
+        pass
71 71
 
72
-	def draw(self, cr):
73
-		"""Draw this shape with the given cairo context"""
74
-		raise NotImplementedError
72
+    def draw(self, cr):
73
+        """Draw this shape with the given cairo context"""
74
+        raise NotImplementedError
75 75
 
76 76
 
77 77
 class TextShape(Shape):
78
-	
79
-	#fontmap = pangocairo.CairoFontMap()
80
-	#fontmap.set_resolution(72)
81
-	#context = fontmap.create_context()
82
-
83
-	LEFT, CENTER, RIGHT = -1, 0, 1
84
-
85
-	def __init__(self, pen, x, y, j, w, t):
86
-		Shape.__init__(self)
87
-		self.pen = pen.copy()
88
-		self.x = x
89
-		self.y = y
90
-		self.j = j
91
-		self.w = w
92
-		self.t = t
93
-
94
-	def draw(self, cr):
95
-
96
-		try:
97
-			layout = self.layout
98
-		except AttributeError:
99
-			layout = cr.create_layout()
100
-			
101
-			# set font options
102
-			# see http://lists.freedesktop.org/archives/cairo/2007-February/009688.html
103
-			context = layout.get_context()
104
-			fo = cairo.FontOptions()
105
-			fo.set_antialias(cairo.ANTIALIAS_DEFAULT)
106
-			fo.set_hint_style(cairo.HINT_STYLE_NONE)
107
-			fo.set_hint_metrics(cairo.HINT_METRICS_OFF)
108
-			pangocairo.context_set_font_options(context, fo)
109
-			
110
-			# set font
111
-			font = pango.FontDescription()
112
-			font.set_family(self.pen.fontname)
113
-			font.set_absolute_size(self.pen.fontsize*pango.SCALE)
114
-			layout.set_font_description(font)
115
-			
116
-			# set text
117
-			layout.set_text(self.t)
118
-			
119
-			# cache it
120
-			self.layout = layout
121
-		else:
122
-			cr.update_layout(layout)
123
-
124
-		width, height = layout.get_size()
125
-		width = float(width)/pango.SCALE
126
-		height = float(height)/pango.SCALE
127
-
128
-		cr.move_to(self.x - self.w/2, self.y)
129
-
130
-		if self.j == self.LEFT:
131
-			x = self.x
132
-		elif self.j == self.CENTER:
133
-			x = self.x - 0.5*width
134
-		elif self.j == self.RIGHT:
135
-			x = self.x - width
136
-		else:
137
-			assert 0
138
-		
139
-		y = self.y - height
140
-		
141
-		cr.move_to(x, y)
142
-
143
-		cr.set_source_rgba(*self.pen.color)
144
-		cr.show_layout(layout)
78
+    
79
+    #fontmap = pangocairo.CairoFontMap()
80
+    #fontmap.set_resolution(72)
81
+    #context = fontmap.create_context()
82
+
83
+    LEFT, CENTER, RIGHT = -1, 0, 1
84
+
85
+    def __init__(self, pen, x, y, j, w, t):
86
+        Shape.__init__(self)
87
+        self.pen = pen.copy()
88
+        self.x = x
89
+        self.y = y
90
+        self.j = j
91
+        self.w = w
92
+        self.t = t
93
+
94
+    def draw(self, cr):
95
+
96
+        try:
97
+            layout = self.layout
98
+        except AttributeError:
99
+            layout = cr.create_layout()
100
+            
101
+            # set font options
102
+            # see http://lists.freedesktop.org/archives/cairo/2007-February/009688.html
103
+            context = layout.get_context()
104
+            fo = cairo.FontOptions()
105
+            fo.set_antialias(cairo.ANTIALIAS_DEFAULT)
106
+            fo.set_hint_style(cairo.HINT_STYLE_NONE)
107
+            fo.set_hint_metrics(cairo.HINT_METRICS_OFF)
108
+            pangocairo.context_set_font_options(context, fo)
109
+            
110
+            # set font
111
+            font = pango.FontDescription()
112
+            font.set_family(self.pen.fontname)
113
+            font.set_absolute_size(self.pen.fontsize*pango.SCALE)
114
+            layout.set_font_description(font)
115
+            
116
+            # set text
117
+            layout.set_text(self.t)
118
+            
119
+            # cache it
120
+            self.layout = layout
121
+        else:
122
+            cr.update_layout(layout)
123
+
124
+        width, height = layout.get_size()
125
+        width = float(width)/pango.SCALE
126
+        height = float(height)/pango.SCALE
127
+
128
+        cr.move_to(self.x - self.w/2, self.y)
129
+
130
+        if self.j == self.LEFT:
131
+            x = self.x
132
+        elif self.j == self.CENTER:
133
+            x = self.x - 0.5*width
134
+        elif self.j == self.RIGHT:
135
+            x = self.x - width
136
+        else:
137
+            assert 0
138
+        
139
+        y = self.y - height
140
+        
141
+        cr.move_to(x, y)
142
+
143
+        cr.set_source_rgba(*self.pen.color)
144
+        cr.show_layout(layout)
145 145
 
146 146
 
147 147
 class EllipseShape(Shape):
148 148
 
149
-	def __init__(self, pen, x0, y0, w, h, filled=False):
150
-		Shape.__init__(self)
151
-		self.pen = pen.copy()
152
-		self.x0 = x0
153
-		self.y0 = y0
154
-		self.w = w
155
-		self.h = h
156
-		self.filled = filled
157
-
158
-	def draw(self, cr):
159
-		cr.save()
160
-		cr.translate(self.x0, self.y0)
161
-		cr.scale(self.w, self.h)
162
-		cr.move_to(1.0, 0.0)
163
-		cr.arc(0.0, 0.0, 1.0, 0, 2.0*math.pi)
164
-		cr.restore()
165
-		if self.filled:
166
-			cr.set_source_rgba(*self.pen.fillcolor)
167
-			cr.fill()
168
-		else:
169
-			cr.set_line_width(self.pen.linewidth)
170
-			cr.set_source_rgba(*self.pen.color)
171
-			cr.stroke()
149
+    def __init__(self, pen, x0, y0, w, h, filled=False):
150
+        Shape.__init__(self)
151
+        self.pen = pen.copy()
152
+        self.x0 = x0
153
+        self.y0 = y0
154
+        self.w = w
155
+        self.h = h
156
+        self.filled = filled
157
+
158
+    def draw(self, cr):
159
+        cr.save()
160
+        cr.translate(self.x0, self.y0)
161
+        cr.scale(self.w, self.h)
162
+        cr.move_to(1.0, 0.0)
163
+        cr.arc(0.0, 0.0, 1.0, 0, 2.0*math.pi)
164
+        cr.restore()
165
+        if self.filled:
166
+            cr.set_source_rgba(*self.pen.fillcolor)
167
+            cr.fill()
168
+        else:
169
+            cr.set_line_width(self.pen.linewidth)
170
+            cr.set_source_rgba(*self.pen.color)
171
+            cr.stroke()
172 172
 
173 173
 
174 174
 class PolygonShape(Shape):
175 175
 
176
-	def __init__(self, pen, points, filled=False):
177
-		Shape.__init__(self)
178
-		self.pen = pen.copy()
179
-		self.points = points
180
-		self.filled = filled
181
-
182
-	def draw(self, cr):
183
-		x0, y0 = self.points[-1]
184
-		cr.move_to(x0, y0)
185
-		for x, y in self.points:
186
-			cr.line_to(x, y)
187
-		cr.close_path()
188
-		if self.filled:
189
-			cr.set_source_rgba(*self.pen.fillcolor)
190
-			cr.fill_preserve()
191
-			cr.fill()
192
-		else:
193
-			cr.set_line_width(self.pen.linewidth)
194
-			cr.set_source_rgba(*self.pen.color)
195
-			cr.stroke()
176
+    def __init__(self, pen, points, filled=False):
177
+        Shape.__init__(self)
178
+        self.pen = pen.copy()
179
+        self.points = points
180
+        self.filled = filled
181
+
182
+    def draw(self, cr):
183
+        x0, y0 = self.points[-1]
184
+        cr.move_to(x0, y0)
185
+        for x, y in self.points:
186
+            cr.line_to(x, y)
187
+        cr.close_path()
188
+        if self.filled:
189
+            cr.set_source_rgba(*self.pen.fillcolor)
190
+            cr.fill_preserve()
191
+            cr.fill()
192
+        else:
193
+            cr.set_line_width(self.pen.linewidth)
194
+            cr.set_source_rgba(*self.pen.color)
195
+            cr.stroke()
196 196
 
197 197
 
198 198
 class BezierShape(Shape):
199 199
 
200
-	def __init__(self, pen, points):
201
-		Shape.__init__(self)
202
-		self.pen = pen.copy()
203
-		self.points = points
200
+    def __init__(self, pen, points):
201
+        Shape.__init__(self)
202
+        self.pen = pen.copy()
203
+        self.points = points
204 204
 
205
-	def draw(self, cr):
206
-		x0, y0 = self.points[0]
207
-		cr.move_to(x0, y0)
208
-		for i in xrange(1, len(self.points), 3):
209
-			x1, y1 = self.points[i]
210
-			x2, y2 = self.points[i + 1]
211
-			x3, y3 = self.points[i + 2]
212
-			cr.curve_to(x1, y1, x2, y2, x3, y3)
213
-		cr.set_line_width(self.pen.linewidth)
214
-		cr.set_source_rgba(*self.pen.color)
215
-		cr.stroke()
205
+    def draw(self, cr):
206
+        x0, y0 = self.points[0]
207
+        cr.move_to(x0, y0)
208
+        for i in xrange(1, len(self.points), 3):
209
+            x1, y1 = self.points[i]
210
+            x2, y2 = self.points[i + 1]
211
+            x3, y3 = self.points[i + 2]
212
+            cr.curve_to(x1, y1, x2, y2, x3, y3)
213
+        cr.set_line_width(self.pen.linewidth)
214
+        cr.set_source_rgba(*self.pen.color)
215
+        cr.stroke()
216 216
 
217 217
 
218 218
 class CompoundShape(Shape):
219 219
 
220
-	def __init__(self, shapes):
221
-		Shape.__init__(self)
222
-		self.shapes = shapes
220
+    def __init__(self, shapes):
221
+        Shape.__init__(self)
222
+        self.shapes = shapes
223 223
 
224
-	def draw(self, cr):
225
-		for shape in self.shapes:
226
-			shape.draw(cr)
224
+    def draw(self, cr):
225
+        for shape in self.shapes:
226
+            shape.draw(cr)
227 227
 
228 228
 
229 229
 class Element(CompoundShape):
230
-	"""Base class for graph nodes and edges."""
230
+    """Base class for graph nodes and edges."""
231 231
 
232
-	def __init__(self, shapes):
233
-		CompoundShape.__init__(self, shapes)
234
-	
235
-	def get_url(self, x, y):
236
-		return None
232
+    def __init__(self, shapes):
233
+        CompoundShape.__init__(self, shapes)
234
+    
235
+    def get_url(self, x, y):
236
+        return None
237 237
 
238
-	def get_jump(self, x, y):
239
-		return None
238
+    def get_jump(self, x, y):
239
+        return None
240 240
 
241 241
 
242 242
 class Node(Element):
243 243
 
244
-	def __init__(self, x, y, w, h, shapes, url):
245
-		Element.__init__(self, shapes)
246
-		
247
-		self.x = x
248
-		self.y = y
244
+    def __init__(self, x, y, w, h, shapes, url):
245
+        Element.__init__(self, shapes)
246
+        
247
+        self.x = x
248
+        self.y = y
249 249
 
250
-		self.x1 = x - 0.5*w
251
-		self.y1 = y - 0.5*h
252
-		self.x2 = x + 0.5*w
253
-		self.y2 = y + 0.5*h
254
-		
255
-		self.url = url
250
+        self.x1 = x - 0.5*w
251
+        self.y1 = y - 0.5*h
252
+        self.x2 = x + 0.5*w
253
+        self.y2 = y + 0.5*h
254
+        
255
+        self.url = url
256 256
 
257
-	def is_inside(self, x, y):
258
-		return self.x1 <= x and x <= self.x2 and self.y1 <= y and y <= self.y2
257
+    def is_inside(self, x, y):
258
+        return self.x1 <= x and x <= self.x2 and self.y1 <= y and y <= self.y2
259 259
 
260
-	def get_url(self, x, y):
261
-		if self.url is None:
262
-			return None
263
-		#print (x, y), (self.x1, self.y1), "-", (self.x2, self.y2)
264
-		if self.is_inside(x, y):
265
-			return self.url
266
-		return None
260
+    def get_url(self, x, y):
261
+        if self.url is None:
262
+            return None
263
+        #print (x, y), (self.x1, self.y1), "-", (self.x2, self.y2)
264
+        if self.is_inside(x, y):
265
+            return self.url
266
+        return None
267 267
 
268
-	def get_jump(self, x, y):
269
-		if self.is_inside(x, y):
270
-			return self.x, self.y
271
-		return None
268
+    def get_jump(self, x, y):
269
+        if self.is_inside(x, y):
270
+            return self.x, self.y
271
+        return None
272 272
 
273 273
 
274 274
 def square_distance(x1, y1, x2, y2):
275
-	deltax = x2 - x1
276
-	deltay = y2 - y1
277
-	return deltax*deltax + deltay*deltay
275
+    deltax = x2 - x1
276
+    deltay = y2 - y1
277
+    return deltax*deltax + deltay*deltay
278 278
 
279 279
 
280 280
 class Edge(Element):
281 281
 
282
-	def __init__(self, points, shapes):
283
-		Element.__init__(self, shapes)
282
+    def __init__(self, points, shapes):
283
+        Element.__init__(self, shapes)
284 284
 
285
-		self.points = points
285
+        self.points = points
286 286
 
287
-	RADIUS = 10
287
+    RADIUS = 10
288 288
 
289
-	def get_jump(self, x, y):
290
-		if square_distance(x, y, *self.points[0]) <= self.RADIUS*self.RADIUS:
291
-			return self.points[-1]
292
-		if square_distance(x, y, *self.points[-1]) <= self.RADIUS*self.RADIUS:
293
-			return self.points[0]
294
-		return None
289
+    def get_jump(self, x, y):
290
+        if square_distance(x, y, *self.points[0]) <= self.RADIUS*self.RADIUS:
291
+            return self.points[-1]
292
+        if square_distance(x, y, *self.points[-1]) <= self.RADIUS*self.RADIUS:
293
+            return self.points[0]
294
+        return None
295 295
 
296 296
 
297 297
 class Graph(Shape):
298 298
 
299
-	def __init__(self, width=1, height=1, nodes=(), edges=()):
300
-		Shape.__init__(self)
301
-		
302
-		self.width = width
303
-		self.height = height
304
-		self.nodes = nodes
305
-		self.edges = edges
306
-
307
-	def get_size(self):
308
-		return self.width, self.height
309
-
310
-	def draw(self, cr):
311
-		cr.set_source_rgba(0.0, 0.0, 0.0, 1.0)
312
-
313
-		cr.set_line_cap(cairo.LINE_CAP_BUTT)
314
-		cr.set_line_join(cairo.LINE_JOIN_MITER)
315
-		
316
-		for edge in self.edges:
317
-			edge.draw(cr)
318
-		for node in self.nodes:
319
-			node.draw(cr)
320
-	
321
-	def get_url(self, x, y):
322
-		for node in self.nodes:
323
-			url = node.get_url(x, y)
324
-			if url is not None:
325
-				return url
326
-		return None
327
-
328
-	def get_jump(self, x, y):
329
-		for edge in self.edges:
330
-			jump = edge.get_jump(x, y)
331
-			if jump is not None:
332
-				return jump
333
-		for node in self.nodes:
334
-			jump = node.get_jump(x, y)
335
-			if jump is not None:
336
-				return jump
337
-		return None
299
+    def __init__(self, width=1, height=1, nodes=(), edges=()):
300
+        Shape.__init__(self)
301
+        
302
+        self.width = width
303
+        self.height = height
304
+        self.nodes = nodes
305
+        self.edges = edges
306
+
307
+    def get_size(self):
308
+        return self.width, self.height
309
+
310
+    def draw(self, cr):
311
+        cr.set_source_rgba(0.0, 0.0, 0.0, 1.0)
312
+
313
+        cr.set_line_cap(cairo.LINE_CAP_BUTT)
314
+        cr.set_line_join(cairo.LINE_JOIN_MITER)
315
+        
316
+        for edge in self.edges:
317
+            edge.draw(cr)
318
+        for node in self.nodes:
319
+            node.draw(cr)
320
+    
321
+    def get_url(self, x, y):
322
+        for node in self.nodes:
323
+            url = node.get_url(x, y)
324
+            if url is not None:
325
+                return url
326
+        return None
327
+
328
+    def get_jump(self, x, y):
329
+        for edge in self.edges:
330
+            jump = edge.get_jump(x, y)
331
+            if jump is not None:
332
+                return jump
333
+        for node in self.nodes:
334
+            jump = node.get_jump(x, y)
335
+            if jump is not None:
336
+                return jump
337
+        return None
338 338
 
339 339
 
340 340
 class XDotAttrParser:
341
-	"""Parser for xdot drawing attributes.
342
-	See also:
343
-	- http://www.graphviz.org/doc/info/output.html#d:xdot
344
-	"""
345
-
346
-	def __init__(self, parser, buf):
347
-		self.parser = parser
348
-		self.buf = self.unescape(buf)
349
-		self.pos = 0
350
-
351
-	def __nonzero__(self):
352
-		return self.pos < len(self.buf)
353
-
354
-	def unescape(self, buf):
355
-		buf = buf.replace('\\"', '"')
356
-		buf = buf.replace('\\n', '\n')
357
-		return buf
358
-
359
-	def read_code(self):
360
-		pos = self.buf.find(" ", self.pos)
361
-		res = self.buf[self.pos:pos]
362
-		self.pos = pos + 1
363
-		while self.pos < len(self.buf) and self.buf[self.pos].isspace():
364
-			self.pos += 1
365
-		return res
366
-
367
-	def read_number(self):
368
-		return int(self.read_code())
369
-
370
-	def read_float(self):
371
-		return float(self.read_code())
372
-
373
-	def read_point(self):
374
-		x = self.read_number()
375
-		y = self.read_number()
376
-		return self.transform(x, y)
377
-
378
-	def read_text(self):
379
-		num = self.read_number()
380
-		pos = self.buf.find("-", self.pos) + 1
381
-		self.pos = pos + num
382
-		res = self.buf[pos:self.pos]
383
-		while self.pos < len(self.buf) and self.buf[self.pos].isspace():
384
-			self.pos += 1
385
-		return res
386
-
387
-	def read_polygon(self):
388
-		n = self.read_number()
389
-		p = []
390
-		for i in range(n):
391
-			x, y = self.read_point()
392
-			p.append((x, y))
393
-		return p
394
-
395
-	def read_color(self):
396
-		# See http://www.graphviz.org/doc/info/attrs.html#k:color
397
-		c = self.read_text()
398
-		c1 = c[:1]
399
-		if c1 == '#':
400
-			hex2float = lambda h: float(int(h, 16)/255.0)
401
-			r = hex2float(c[1:3])
402
-			g = hex2float(c[3:5])
403
-			b = hex2float(c[5:7])
404
-			try:
405
-				a = hex2float(c[7:9])
406
-			except (IndexError, ValueError):
407
-				a = 1.0
408
-			return r, g, b, a
409
-		elif c1.isdigit():
410
-			h, s, v = map(float, c[1:].split(","))
411
-			raise NotImplementedError
412
-		else:
413
-			color = gtk.gdk.color_parse(c)
414
-			s = 1.0/65535.0
415
-			r = color.red*s
416
-			g = color.green*s
417
-			b = color.blue*s
418
-			a = 1.0
419
-			return r, g, b, a
420
-
421
-	def parse(self):
422
-		shapes = []
423
-		pen = Pen()
424
-		s = self
425
-
426
-		while s:
427
-			op = s.read_code()
428
-			if op == "c":
429
-				pen.color = s.read_color()
430
-			elif op == "C":
431
-				pen.fillcolor = s.read_color()
432
-			elif op == "S":
433
-				s.read_text()
434
-			elif op == "F":
435
-				pen.fontsize = s.read_float()
436
-				pen.fontname = s.read_text()
437
-			elif op == "T":
438
-				x, y = s.read_point()
439
-				j = s.read_number()
440
-				w = s.read_number()
441
-				t = s.read_text()
442
-				shapes.append(TextShape(pen, x, y, j, w, t))
443
-			elif op == "E":
444
-				x0, y0 = s.read_point()
445
-				w = s.read_number()
446
-				h = s.read_number()
447
-				shapes.append(EllipseShape(pen, x0, y0, w, h, filled=True))
448
-			elif op == "e":
449
-				x0, y0 = s.read_point()
450
-				w = s.read_number()
451
-				h = s.read_number()
452
-				shapes.append(EllipseShape(pen, x0, y0, w, h))
453
-			elif op == "B":
454
-				p = self.read_polygon()
455
-				shapes.append(BezierShape(pen, p))
456
-			elif op == "P":
457
-				p = self.read_polygon()
458
-				shapes.append(PolygonShape(pen, p, filled=True))
459
-			elif op == "p":
460
-				p = self.read_polygon()
461
-				shapes.append(PolygonShape(pen, p))
462
-			else:
463
-				sys.stderr.write("unknown xdot opcode '%s'\n" % op)
464
-				break
465
-		return shapes
466
-
467
-	def transform(self, x, y):
468
-		return self.parser.transform(x, y)
341
+    """Parser for xdot drawing attributes.
342
+    See also:
343
+    - http://www.graphviz.org/doc/info/output.html#d:xdot
344
+    """
345
+
346
+    def __init__(self, parser, buf):
347
+        self.parser = parser
348
+        self.buf = self.unescape(buf)
349
+        self.pos = 0
350
+
351
+    def __nonzero__(self):
352
+        return self.pos < len(self.buf)
353
+
354
+    def unescape(self, buf):
355
+        buf = buf.replace('\\"', '"')
356
+        buf = buf.replace('\\n', '\n')
357
+        return buf
358
+
359
+    def read_code(self):
360
+        pos = self.buf.find(" ", self.pos)
361
+        res = self.buf[self.pos:pos]
362
+        self.pos = pos + 1
363
+        while self.pos < len(self.buf) and self.buf[self.pos].isspace():
364
+            self.pos += 1
365
+        return res
366
+
367
+    def read_number(self):
368
+        return int(self.read_code())
369
+
370
+    def read_float(self):
371
+        return float(self.read_code())
372
+
373
+    def read_point(self):
374
+        x = self.read_number()
375
+        y = self.read_number()
376
+        return self.transform(x, y)
377
+
378
+    def read_text(self):
379
+        num = self.read_number()
380
+        pos = self.buf.find("-", self.pos) + 1
381
+        self.pos = pos + num
382
+        res = self.buf[pos:self.pos]
383
+        while self.pos < len(self.buf) and self.buf[self.pos].isspace():
384
+            self.pos += 1
385
+        return res
386
+
387
+    def read_polygon(self):
388
+        n = self.read_number()
389
+        p = []
390
+        for i in range(n):
391
+            x, y = self.read_point()
392
+            p.append((x, y))
393
+        return p
394
+
395
+    def read_color(self):
396
+        # See http://www.graphviz.org/doc/info/attrs.html#k:color
397
+        c = self.read_text()
398
+        c1 = c[:1]
399
+        if c1 == '#':
400
+            hex2float = lambda h: float(int(h, 16)/255.0)
401
+            r = hex2float(c[1:3])
402
+            g = hex2float(c[3:5])
403
+            b = hex2float(c[5:7])
404
+            try:
405
+                a = hex2float(c[7:9])
406
+            except (IndexError, ValueError):
407
+                a = 1.0
408
+            return r, g, b, a
409
+        elif c1.isdigit():
410
+            h, s, v = map(float, c[1:].split(","))
411
+            raise NotImplementedError
412
+        else:
413
+            color = gtk.gdk.color_parse(c)
414
+            s = 1.0/65535.0
415
+            r = color.red*s
416
+            g = color.green*s
417
+            b = color.blue*s
418
+            a = 1.0
419
+            return r, g, b, a
420
+
421
+    def parse(self):
422
+        shapes = []
423
+        pen = Pen()
424
+        s = self
425
+
426
+        while s:
427
+            op = s.read_code()
428
+            if op == "c":
429
+                pen.color = s.read_color()
430
+            elif op == "C":
431
+                pen.fillcolor = s.read_color()
432
+            elif op == "S":
433
+                s.read_text()
434
+            elif op == "F":
435
+                pen.fontsize = s.read_float()
436
+                pen.fontname = s.read_text()
437
+            elif op == "T":
438
+                x, y = s.read_point()
439
+                j = s.read_number()
440
+                w = s.read_number()
441
+                t = s.read_text()
442
+                shapes.append(TextShape(pen, x, y, j, w, t))
443
+            elif op == "E":
444
+                x0, y0 = s.read_point()
445
+                w = s.read_number()
446
+                h = s.read_number()
447
+                shapes.append(EllipseShape(pen, x0, y0, w, h, filled=True))
448
+            elif op == "e":
449
+                x0, y0 = s.read_point()
450
+                w = s.read_number()
451
+                h = s.read_number()
452
+                shapes.append(EllipseShape(pen, x0, y0, w, h))
453
+            elif op == "B":
454
+                p = self.read_polygon()
455
+                shapes.append(BezierShape(pen, p))
456
+            elif op == "P":
457
+                p = self.read_polygon()
458
+                shapes.append(PolygonShape(pen, p, filled=True))
459
+            elif op == "p":
460
+                p = self.read_polygon()
461
+                shapes.append(PolygonShape(pen, p))
462
+            else:
463
+                sys.stderr.write("unknown xdot opcode '%s'\n" % op)
464
+                break
465
+        return shapes
466
+
467
+    def transform(self, x, y):
468
+        return self.parser.transform(x, y)
469 469
 
470 470
 
471 471
 class XDotParser:
472
-	
473
-	def __init__(self, xdotcode):
474
-		self.xdotcode = xdotcode
475
-
476
-	def parse(self):
477
-		graph = pydot.graph_from_dot_data(self.xdotcode)
478
-
479
-		bb = graph.get_bb()
480
-		if bb is None:
481
-			return []
482
-
483
-		xmin, ymin, xmax, ymax = map(int, bb.split(","))
484
-
485
-		self.xoffset = -xmin
486
-		self.yoffset = -ymax
487
-		self.xscale = 1.0
488
-		self.yscale = -1.0
489
-		# FIXME: scale from points to pixels
490
-
491
-		width = xmax - xmin
492
-		height = ymax - ymin
493
-
494
-		nodes = []
495
-		edges = []
496
-		
497
-		for node in graph.get_node_list():
498
-			if node.pos is None:
499
-				continue
500
-			x, y = self.parse_node_pos(node.pos)
501
-			w = float(node.width)*72
502
-			h = float(node.height)*72
503
-			shapes = []
504
-			for attr in ("_draw_", "_ldraw_"):
505
-				if hasattr(node, attr):
506
-					parser = XDotAttrParser(self, getattr(node, attr))
507
-					shapes.extend(parser.parse())
508
-			url = node.URL
509
-			if shapes:
510
-				nodes.append(Node(x, y, w, h, shapes, url))
511
-
512
-		for edge in graph.get_edge_list():
513
-			if edge.pos is None:
514
-				continue
515
-			points = self.parse_edge_pos(edge.pos)	
516
-			shapes = []
517
-			for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
518
-				if hasattr(edge, attr):
519
-					parser = XDotAttrParser(self, getattr(edge, attr))
520
-					shapes.extend(parser.parse())
521
-			if shapes:
522
-				edges.append(Edge(points, shapes))
523
-
524
-		return Graph(width, height, nodes, edges)
525
-
526
-	def parse_node_pos(self, pos):
527
-		x, y = pos.split(",")
528
-		return self.transform(float(x), float(y))
529
-
530
-	def parse_edge_pos(self, pos):
531
-		points = []
532
-		for entry in pos.split(' '):
533
-			fields = entry.split(',')
534
-			try:
535
-				x, y = fields
536
-			except ValueError:
537
-				# TODO: handle start/end points
538
-				continue
539
-			else:
540
-				points.append(self.transform(float(x), float(y)))
541
-		return points
542
-
543
-	def transform(self, x, y):
544
-		# XXX: this is not the right place for this code
545
-		x = (x + self.xoffset)*self.xscale
546
-		y = (y + self.yoffset)*self.yscale
547
-		return x, y
472
+    
473
+    def __init__(self, xdotcode):
474
+        self.xdotcode = xdotcode
475
+
476
+    def parse(self):
477
+        graph = pydot.graph_from_dot_data(self.xdotcode)
478
+
479
+        bb = graph.get_bb()
480
+        if bb is None:
481
+            return []
482
+
483
+        xmin, ymin, xmax, ymax = map(int, bb.split(","))
484
+
485
+        self.xoffset = -xmin
486
+        self.yoffset = -ymax
487
+        self.xscale = 1.0
488
+        self.yscale = -1.0
489
+        # FIXME: scale from points to pixels
490
+
491
+        width = xmax - xmin
492
+        height = ymax - ymin
493
+
494
+        nodes = []
495
+        edges = []
496
+        
497
+        for node in graph.get_node_list():
498
+            if node.pos is None:
499
+                continue
500
+            x, y = self.parse_node_pos(node.pos)
501
+            w = float(node.width)*72
502
+            h = float(node.height)*72
503
+            shapes = []
504
+            for attr in ("_draw_", "_ldraw_"):
505
+                if hasattr(node, attr):
506
+                    parser = XDotAttrParser(self, getattr(node, attr))
507
+                    shapes.extend(parser.parse())
508
+            url = node.URL
509
+            if shapes:
510
+                nodes.append(Node(x, y, w, h, shapes, url))
511
+
512
+        for edge in graph.get_edge_list():
513
+            if edge.pos is None:
514
+                continue
515
+            points = self.parse_edge_pos(edge.pos)    
516
+            shapes = []
517
+            for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
518
+                if hasattr(edge, attr):
519
+                    parser = XDotAttrParser(self, getattr(edge, attr))
520
+                    shapes.extend(parser.parse())
521
+            if shapes:
522
+                edges.append(Edge(points, shapes))
523
+
524
+        return Graph(width, height, nodes, edges)
525
+
526
+    def parse_node_pos(self, pos):
527
+        x, y = pos.split(",")
528
+        return self.transform(float(x), float(y))
529
+
530
+    def parse_edge_pos(self, pos):
531
+        points = []
532
+        for entry in pos.split(' '):
533
+            fields = entry.split(',')
534
+            try:
535
+                x, y = fields
536
+            except ValueError:
537
+                # TODO: handle start/end points
538
+                continue
539
+            else:
540
+                points.append(self.transform(float(x), float(y)))
541
+        return points
542
+
543
+    def transform(self, x, y):
544
+        # XXX: this is not the right place for this code
545
+        x = (x + self.xoffset)*self.xscale
546
+        y = (y + self.yoffset)*self.yscale
547
+        return x, y
548 548
 
549 549
 
550 550
 class DotWidget(gtk.DrawingArea):
551
-	"""PyGTK widget that draws dot graphs."""
551
+    """PyGTK widget that draws dot graphs."""
552 552
 
553
-	__gsignals__ = {
554
-		'expose-event': 'override',
555
-		'clicked' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gtk.gdk.Event))
556
-	}
557
-
558
-	def __init__(self):
559
-		gtk.DrawingArea.__init__(self)
560
-
561
-		self.graph = Graph()
562
-
563
-		self.set_flags(gtk.CAN_FOCUS)
564
-
565
-		self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
566
-		self.connect("button-press-event", self.on_area_button_press)
567
-		self.connect("button-release-event", self.on_area_button_release)
568
-		self.add_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
569
-		self.connect("motion-notify-event", self.on_area_motion_notify)
570
-		self.connect("scroll-event", self.on_area_scroll_event)
571
-
572
-		self.connect('key-press-event', self.on_key_press_event)
573
-
574
-		self.x, self.y = 0.0, 0.0
575
-		self.zoom_ratio = 1.0
576
-
577
-	def set_dotcode(self, dotcode):
578
-		p = subprocess.Popen(
579
-			['dot', '-Txdot'],
580
-			stdin=subprocess.PIPE,
581
-			stdout=subprocess.PIPE,
582
-			shell=False,
583
-			universal_newlines=True
584
-		)
585
-		xdotcode = p.communicate(dotcode)[0]
586
-		self.set_xdotcode(xdotcode)
587
-
588
-	def set_xdotcode(self, xdotcode):
589
-		#print xdotcode
590
-		parser = XDotParser(xdotcode)
591
-		self.graph = parser.parse()
592
-		self.zoom_image(self.zoom_ratio, center=True)
593
-
594
-	def do_expose_event(self, event):
595
-		cr = self.window.cairo_create()
596
-
597
-		# set a clip region for the expose event
598
-		cr.rectangle(
599
-			event.area.x, event.area.y,
600
-			event.area.width, event.area.height
601
-		)
602
-		cr.clip()
603
-
604
-		cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
605
-		cr.paint()
606
-
607
-		rect = self.get_allocation()
608
-		cr.translate(0.5*rect.width, 0.5*rect.height)
609
-		cr.scale(self.zoom_ratio, self.zoom_ratio)
610
-		cr.translate(-self.x, -self.y)
611
-
612
-		self.graph.draw(cr)
613
-
614
-		return False
615
-
616
-	def get_current_pos(self):
617
-		return self.x, self.y
618
-
619
-	def set_current_pos(self, x, y):
620
-		self.x = x
621
-		self.y = y
622
-		self.queue_draw()
623
-
624
-	def zoom_image(self, zoom_ratio, center=False):
625
-		if center:
626
-			self.x = self.graph.width/2
627
-			self.y = self.graph.height/2
628
-		self.zoom_ratio = zoom_ratio
629
-		self.queue_draw()
630
-
631
-	ZOOM_INCREMENT = 1.25
632
-
633
-	def on_zoom_in(self, action):
634
-		self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
635
-
636
-	def on_zoom_out(self, action):
637
-		self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
638
-
639
-	def on_zoom_fit(self, action):
640
-		rect = self.get_allocation()
641
-		zoom_ratio = min(
642
-			float(rect.width)/float(self.graph.width),
643
-			float(rect.height)/float(self.graph.height)
644
-		)
645
-		self.zoom_image(zoom_ratio, center=True)
646
-
647
-	def on_zoom_100(self, action):
648
-		self.zoom_image(1.0)
649
-
650
-	POS_INCREMENT = 100
651
-
652
-	def on_key_press_event(self, widget, event):
653
-		if event.keyval == gtk.keysyms.Left:
654
-			self.x -= self.POS_INCREMENT/self.zoom_ratio
655
-			self.queue_draw()
656
-			return True
657
-		if event.keyval == gtk.keysyms.Right:
658
-			self.x += self.POS_INCREMENT/self.zoom_ratio
659
-			self.queue_draw()
660
-			return True
661
-		if event.keyval == gtk.keysyms.Up:
662
-			self.y -= self.POS_INCREMENT/self.zoom_ratio
663
-			self.queue_draw()
664
-			return True
665
-		if event.keyval == gtk.keysyms.Down:
666
-			self.y += self.POS_INCREMENT/self.zoom_ratio
667
-			self.queue_draw()
668
-			return True
669
-		if event.keyval == gtk.keysyms.Page_Up:
670
-			self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
671
-			self.queue_draw()
672
-			return True
673
-		if event.keyval == gtk.keysyms.Page_Down:
674
-			self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
675
-			self.queue_draw()
676
-			return True
677
-		return False
678
-
679
-	def on_area_button_press(self, area, event):
680
-		if event.button == 2 or event.button == 1:
681
-			area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
682
-			self.prevmousex = event.x
683
-			self.prevmousey = event.y
684
-
685
-		if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk.BUTTON_RELEASE):
686
-			return False
687
-		x, y = int(event.x), int(event.y)
688
-		url = self.get_url(x, y)
689
-		if url is not None:
690
-			self.emit('clicked', unicode(url), event)
691
-			return True
692
-
693
-		jump = self.get_jump(x, y)
694
-		x, y = self.window2graph(x, y)
695
-		if jump is not None:
696
-			jumpx, jumpy = jump
697
-			self.x += jumpx - x
698
-			self.y += jumpy - y
699
-			self.queue_draw()
700
-		return False
701
-
702
-	def on_area_button_release(self, area, event):
703
-		if event.button == 2 or event.button == 1:
704
-			area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
705
-			self.prevmousex = None
706
-			self.prevmousey = None
707
-			return True
708
-		return False
709
-
710
-	def on_area_scroll_event(self, area, event):
711
-		if event.direction == gtk.gdk.SCROLL_UP:
712
-			self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
713
-			return True
714
-		if event.direction == gtk.gdk.SCROLL_DOWN:
715
-			self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
716
-			return True
717
-		return False
718
-
719
-	def on_area_motion_notify(self, area, event):
720
-		x, y = int(event.x), int(event.y)
721
-		state = event.state
722
-
723
-		if state & gtk.gdk.BUTTON2_MASK or state & gtk.gdk.BUTTON1_MASK:
724
-			# pan the image
725
-			self.x += (self.prevmousex - x)/self.zoom_ratio
726
-			self.y += (self.prevmousey - y)/self.zoom_ratio
727
-			self.queue_draw()
728
-			self.prevmousex = x
729
-			self.prevmousey = y
730
-		else:
731
-			# set cursor
732
-			if self.get_url(x, y) is not None:
733
-				area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
734
-			else:
735
-				area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
736
-
737
-		return True
738
-
739
-	def window2graph(self, x, y):
740
-		rect = self.get_allocation()
741
-		x -= 0.5*rect.width
742
-		y -= 0.5*rect.height
743
-		x /= self.zoom_ratio
744
-		y /= self.zoom_ratio
745
-		x += self.x
746
-		y += self.y
747
-		return x, y
748
-
749
-	def get_url(self, x, y):
750
-		x, y = self.window2graph(x, y)
751
-		return self.graph.get_url(x, y)
752
-
753
-	def get_jump(self, x, y):
754
-		x, y = self.window2graph(x, y)
755
-		return self.graph.get_jump(x, y)
553
+    __gsignals__ = {
554
+        'expose-event': 'override',
555
+        'clicked' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gtk.gdk.Event))
556
+    }
557
+
558
+    def __init__(self):
559
+        gtk.DrawingArea.__init__(self)
560
+
561
+        self.graph = Graph()
562
+
563
+        self.set_flags(gtk.CAN_FOCUS)
564
+
565
+        self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
566
+        self.connect("button-press-event", self.on_area_button_press)
567
+        self.connect("button-release-event", self.on_area_button_release)
568
+        self.add_events(gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
569
+        self.connect("motion-notify-event", self.on_area_motion_notify)
570
+        self.connect("scroll-event", self.on_area_scroll_event)
571
+
572
+        self.connect('key-press-event', self.on_key_press_event)
573
+
574
+        self.x, self.y = 0.0, 0.0
575
+        self.zoom_ratio = 1.0
576
+
577
+    def set_dotcode(self, dotcode):
578
+        p = subprocess.Popen(
579
+            ['dot', '-Txdot'],
580
+            stdin=subprocess.PIPE,
581
+            stdout=subprocess.PIPE,
582
+            shell=False,
583
+            universal_newlines=True
584
+        )
585
+        xdotcode = p.communicate(dotcode)[0]
586
+        self.set_xdotcode(xdotcode)
587
+
588
+    def set_xdotcode(self, xdotcode):
589
+        #print xdotcode
590
+        parser = XDotParser(xdotcode)
591
+        self.graph = parser.parse()
592
+        self.zoom_image(self.zoom_ratio, center=True)
593
+
594
+    def do_expose_event(self, event):
595
+        cr = self.window.cairo_create()
596
+
597
+        # set a clip region for the expose event
598
+        cr.rectangle(
599
+            event.area.x, event.area.y,
600
+            event.area.width, event.area.height
601
+        )
602
+        cr.clip()
603
+
604
+        cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
605
+        cr.paint()
606
+
607
+        rect = self.get_allocation()
608
+        cr.translate(0.5*rect.width, 0.5*rect.height)
609
+        cr.scale(self.zoom_ratio, self.zoom_ratio)
610
+        cr.translate(-self.x, -self.y)
611
+
612
+        self.graph.draw(cr)
613
+
614
+        return False
615
+
616
+    def get_current_pos(self):
617
+        return self.x, self.y
618
+
619
+    def set_current_pos(self, x, y):
620
+        self.x = x
621
+        self.y = y
622
+        self.queue_draw()
623
+
624
+    def zoom_image(self, zoom_ratio, center=False):
625
+        if center:
626
+            self.x = self.graph.width/2
627
+            self.y = self.graph.height/2
628
+        self.zoom_ratio = zoom_ratio
629
+        self.queue_draw()
630
+
631
+    ZOOM_INCREMENT = 1.25
632
+
633
+    def on_zoom_in(self, action):
634
+        self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
635
+
636
+    def on_zoom_out(self, action):
637
+        self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
638
+
639
+    def on_zoom_fit(self, action):
640
+        rect = self.get_allocation()
641
+        zoom_ratio = min(
642
+            float(rect.width)/float(self.graph.width),
643
+            float(rect.height)/float(self.graph.height)
644
+        )
645
+        self.zoom_image(zoom_ratio, center=True)
646
+
647
+    def on_zoom_100(self, action):
648
+        self.zoom_image(1.0)
649
+
650
+    POS_INCREMENT = 100
651
+
652
+    def on_key_press_event(self, widget, event):
653
+        if event.keyval == gtk.keysyms.Left:
654
+            self.x -= self.POS_INCREMENT/self.zoom_ratio
655
+            self.queue_draw()
656
+            return True
657
+        if event.keyval == gtk.keysyms.Right:
658
+            self.x += self.POS_INCREMENT/self.zoom_ratio
659
+            self.queue_draw()
660
+            return True
661
+        if event.keyval == gtk.keysyms.Up:
662
+            self.y -= self.POS_INCREMENT/self.zoom_ratio
663
+            self.queue_draw()
664
+            return True
665
+        if event.keyval == gtk.keysyms.Down:
666
+            self.y += self.POS_INCREMENT/self.zoom_ratio
667
+            self.queue_draw()
668
+            return True
669
+        if event.keyval == gtk.keysyms.Page_Up:
670
+            self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
671
+            self.queue_draw()
672
+            return True
673
+        if event.keyval == gtk.keysyms.Page_Down:
674
+            self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
675
+            self.queue_draw()
676
+            return True
677
+        return False
678
+
679
+    def on_area_button_press(self, area, event):
680
+        if event.button == 2 or event.button == 1:
681
+            area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
682
+            self.prevmousex = event.x
683
+            self.prevmousey = event.y
684
+
685
+        if event.type not in (gtk.gdk.BUTTON_PRESS, gtk.gdk.BUTTON_RELEASE):
686
+            return False
687
+        x, y = int(event.x), int(event.y)
688
+        url = self.get_url(x, y)
689
+        if url is not None:
690
+            self.emit('clicked', unicode(url), event)
691
+            return True
692
+
693
+        jump = self.get_jump(x, y)
694
+        x, y = self.window2graph(x, y)
695
+        if jump is not None:
696
+            jumpx, jumpy = jump
697
+            self.x += jumpx - x
698
+            self.y += jumpy - y
699
+            self.queue_draw()
700
+        return False
701
+
702
+    def on_area_button_release(self, area, event):
703
+        if event.button == 2 or event.button == 1:
704
+            area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
705
+            self.prevmousex = None
706
+            self.prevmousey = None
707
+            return True
708
+        return False
709
+
710
+    def on_area_scroll_event(self, area, event):
711
+        if event.direction == gtk.gdk.SCROLL_UP:
712
+            self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
713
+            return True
714
+        if event.direction == gtk.gdk.SCROLL_DOWN:
715
+            self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
716
+            return True
717
+        return False
718
+
719
+    def on_area_motion_notify(self, area, event):
720
+        x, y = int(event.x), int(event.y)
721
+        state = event.state
722
+
723
+        if state & gtk.gdk.BUTTON2_MASK or state & gtk.gdk.BUTTON1_MASK:
724
+            # pan the image
725
+            self.x += (self.prevmousex - x)/self.zoom_ratio
726
+            self.y += (self.prevmousey - y)/self.zoom_ratio
727
+            self.queue_draw()
728
+            self.prevmousex = x
729
+            self.prevmousey = y
730
+        else:
731
+            # set cursor
732
+            if self.get_url(x, y) is not None:
733
+                area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
734
+            else:
735
+                area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
736
+
737
+        return True
738
+
739
+    def window2graph(self, x, y):
740
+        rect = self.get_allocation()
741
+        x -= 0.5*rect.width
742
+        y -= 0.5*rect.height
743
+        x /= self.zoom_ratio
744
+        y /= self.zoom_ratio
745
+        x += self.x
746
+        y += self.y
747
+        return x, y
748
+
749
+    def get_url(self, x, y):
750
+        x, y = self.window2graph(x, y)
751
+        return self.graph.get_url(x, y)
752
+
753
+    def get_jump(self, x, y):
754
+        x, y = self.window2graph(x, y)
755
+        return self.graph.get_jump(x, y)
756 756
 
757 757
 
758 758
 class DotWindow(gtk.Window):
759 759
 
760
-	ui = '''
761
-	<ui>
762
-		<toolbar name="ToolBar">
763
-			<toolitem action="ZoomIn"/>
764
-			<toolitem action="ZoomOut"/>
765
-			<toolitem action="ZoomFit"/>
766
-			<toolitem action="Zoom100"/>
767
-		</toolbar>
768
-	</ui>
769
-	'''
760
+    ui = '''
761
+    <ui>
762
+        <toolbar name="ToolBar">
763
+            <toolitem action="ZoomIn"/>
764
+            <toolitem action="ZoomOut"/>
765
+            <toolitem action="ZoomFit"/>
766
+            <toolitem action="Zoom100"/>
767
+        </toolbar>
768
+    </ui>
769
+    '''
770 770
 
771
-	def __init__(self):
772
-		gtk.Window.__init__(self)
771
+    def __init__(self):
772
+        gtk.Window.__init__(self)
773 773
 
774
-		self.graph = Graph()
774
+        self.graph = Graph()
775 775
 
776
-		window = self
776
+        window = self
777 777
 
778
-		window.set_title('Dot')
779
-		window.set_default_size(512, 512)
780
-		vbox = gtk.VBox()
781
-		window.add(vbox)
778
+        window.set_title('Dot')
779
+        window.set_default_size(512, 512)
780
+        vbox = gtk.VBox()
781
+        window.add(vbox)
782 782
 
783
-		self.widget = DotWidget()
783
+        self.widget = DotWidget()
784 784
 
785
-		# Create a UIManager instance
786
-		uimanager = self.uimanager = gtk.UIManager()
785
+        # Create a UIManager instance
786
+        uimanager = self.uimanager = gtk.UIManager()
787 787
 
788
-		# Add the accelerator group to the toplevel window
789
-		accelgroup = uimanager.get_accel_group()
790
-		window.add_accel_group(accelgroup)
788
+        # Add the accelerator group to the toplevel window
789
+        accelgroup = uimanager.get_accel_group()
790
+        window.add_accel_group(accelgroup)
791 791
 
792
-		# Create an ActionGroup
793
-		actiongroup = gtk.ActionGroup('Actions')
794
-		self.actiongroup = actiongroup
792
+        # Create an ActionGroup
793
+        actiongroup = gtk.ActionGroup('Actions')
794
+        self.actiongroup = actiongroup
795 795
 
796
-		# Create actions
797
-		actiongroup.add_actions((
798
-			('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.widget.on_zoom_in),
799
-			('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.widget.on_zoom_out),
800
-			('ZoomFit', gtk.STOCK_ZOOM_FIT, None, None, None, self.widget.on_zoom_fit),
801
-			('Zoom100', gtk.STOCK_ZOOM_100, None, None, None, self.widget.on_zoom_100),
802
-		))
796
+        # Create actions
797
+        actiongroup.add_actions((
798
+            ('ZoomIn', gtk.STOCK_ZOOM_IN, None, None, None, self.widget.on_zoom_in),
799
+            ('ZoomOut', gtk.STOCK_ZOOM_OUT, None, None, None, self.widget.on_zoom_out),
800
+            ('ZoomFit', gtk.STOCK_ZOOM_FIT, None, None, None, self.widget.on_zoom_fit),
801
+            ('Zoom100', gtk.STOCK_ZOOM_100, None, None, None, self.widget.on_zoom_100),
802
+        ))
803 803
 
804
-		# Add the actiongroup to the uimanager
805
-		uimanager.insert_action_group(actiongroup, 0)
804
+        # Add the actiongroup to the uimanager
805
+        uimanager.insert_action_group(actiongroup, 0)
806 806
 
807
-		# Add a UI description
808
-		uimanager.add_ui_from_string(self.ui)
807
+        # Add a UI description
808
+        uimanager.add_ui_from_string(self.ui)
809 809
 
810
-		# Create a Toolbar
811
-		toolbar = uimanager.get_widget('/ToolBar')
812
-		vbox.pack_start(toolbar, False)
810
+        # Create a Toolbar
811
+        toolbar = uimanager.get_widget('/ToolBar')
812
+        vbox.pack_start(toolbar, False)
813 813
 
814
-		vbox.pack_start(self.widget)
814
+        vbox.pack_start(self.widget)
815 815
 
816
-		self.set_focus(self.widget)
816
+        self.set_focus(self.widget)
817 817
 
818
-		self.show_all()
818
+        self.show_all()
819 819
 
820
-	def set_dotcode(self, dotcode):
821
-		self.widget.set_dotcode(dotcode)
820
+    def set_dotcode(self, dotcode):
821
+        self.widget.set_dotcode(dotcode)
822 822
 
823 823
 
824 824
 def main():
825
-	import optparse
826
-	
827
-	parser = optparse.OptionParser(
828
-		usage="\n\t%prog [file]",
829
-		version="%%prog %s" % __version__)
830
-	
831
-	(options, args) = parser.parse_args(sys.argv[1:])
832
-	
833
-	if len(args) == 0:
834
-		fp = sys.stdin
835
-	elif len(args) == 1:
836
-		fp = file(args[0], 'rt')
837
-	else:
838
-		parser.error('incorrect number of arguments')
839
-	
840
-	win = DotWindow()
841
-	win.set_dotcode(fp.read())
842
-	win.connect('destroy', gtk.main_quit)
843
-	gtk.main()
825
+    import optparse
826
+    
827
+    parser = optparse.OptionParser(
828
+        usage="\n\t%prog [file]",
829
+        version="%%prog %s" % __version__)
830
+    
831
+    (options, args) = parser.parse_args(sys.argv[1:])
832
+    
833
+    if len(args) == 0:
834
+        fp = sys.stdin
835
+    elif len(args) == 1:
836
+        fp = file(args[0], 'rt')
837
+    else:
838
+        parser.error('incorrect number of arguments')
839
+    
840
+    win = DotWindow()
841
+    win.set_dotcode(fp.read())
842
+    win.connect('destroy', gtk.main_quit)
843
+    gtk.main()
844 844
 
845 845
 
846 846
 if __name__ == '__main__':
847
-	main()
847
+    main()