Browse code

Cleaner splitting into separate modules

Peter Hill authored on 02/07/2016 09:45:05 • Jose Fonseca committed on 10/07/2016 08:40:15
Showing 15 changed files

... ...
@@ -32,8 +32,9 @@ setup(
32 32
         """,
33 33
     license="LGPL",
34 34
 
35
-    py_modules=['xdot'],
36
-    entry_points=dict(gui_scripts=['xdot=xdot:main']),
35
+    install_requires=['graphviz'],
36
+    packages=['xdot'],
37
+    entry_points=dict(gui_scripts=['xdot=xdot.__main__:main']),
37 38
 
38 39
     # https://pypi.python.org/pypi?%3Aaction=list_classifiers
39 40
     classifiers=[
40 41
deleted file mode 100755
... ...
@@ -1,2512 +0,0 @@
1
-#!/usr/bin/env python3
2
-#
3
-# Copyright 2008-2015 Jose Fonseca
4
-#
5
-# This program is free software: you can redistribute it and/or modify it
6
-# under the terms of the GNU Lesser General Public License as published
7
-# by the Free Software Foundation, either version 3 of the License, or
8
-# (at your option) any later version.
9
-#
10
-# This program is distributed in the hope that it will be useful,
11
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
-# GNU Lesser General Public License for more details.
14
-#
15
-# You should have received a copy of the GNU Lesser General Public License
16
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
-#
18
-
19
-'''Visualize dot graphs via the xdot format.'''
20
-
21
-__author__ = "Jose Fonseca et al"
22
-
23
-
24
-import os
25
-import sys
26
-import subprocess
27
-import math
28
-import colorsys
29
-import time
30
-import re
31
-import optparse
32
-
33
-import gi
34
-gi.require_version('Gtk', '3.0')
35
-gi.require_version('PangoCairo', '1.0')
36
-
37
-from gi.repository import GLib
38
-from gi.repository import GObject
39
-from gi.repository import Gtk
40
-from gi.repository import Gdk
41
-from gi.repository import GdkPixbuf
42
-from gi.repository import Pango
43
-from gi.repository import PangoCairo
44
-import cairo
45
-
46
-
47
-# See http://www.graphviz.org/pub/scm/graphviz-cairo/plugin/cairo/gvrender_cairo.c
48
-
49
-# For pygtk inspiration and guidance see:
50
-# - http://mirageiv.berlios.de/
51
-# - http://comix.sourceforge.net/
52
-
53
-
54
-class Pen:
55
-    """Store pen attributes."""
56
-
57
-    def __init__(self):
58
-        # set default attributes
59
-        self.color = (0.0, 0.0, 0.0, 1.0)
60
-        self.fillcolor = (0.0, 0.0, 0.0, 1.0)
61
-        self.linewidth = 1.0
62
-        self.fontsize = 14.0
63
-        self.fontname = "Times-Roman"
64
-        self.bold = False
65
-        self.italic = False
66
-        self.underline = False
67
-        self.superscript = False
68
-        self.subscript = False
69
-        self.strikethrough = False
70
-        self.overline = False
71
-
72
-        self.dash = ()
73
-
74
-    def copy(self):
75
-        """Create a copy of this pen."""
76
-        pen = Pen()
77
-        pen.__dict__ = self.__dict__.copy()
78
-        return pen
79
-
80
-    def highlighted(self):
81
-        pen = self.copy()
82
-        pen.color = (1, 0, 0, 1)
83
-        pen.fillcolor = (1, .8, .8, 1)
84
-        return pen
85
-
86
-
87
-class Shape:
88
-    """Abstract base class for all the drawing shapes."""
89
-
90
-    def __init__(self):
91
-        pass
92
-
93
-    def draw(self, cr, highlight=False):
94
-        """Draw this shape with the given cairo context"""
95
-        raise NotImplementedError
96
-
97
-    def select_pen(self, highlight):
98
-        if highlight:
99
-            if not hasattr(self, 'highlight_pen'):
100
-                self.highlight_pen = self.pen.highlighted()
101
-            return self.highlight_pen
102
-        else:
103
-            return self.pen
104
-
105
-    def search_text(self, regexp):
106
-        return False
107
-
108
-
109
-class TextShape(Shape):
110
-
111
-    LEFT, CENTER, RIGHT = -1, 0, 1
112
-
113
-    def __init__(self, pen, x, y, j, w, t):
114
-        Shape.__init__(self)
115
-        self.pen = pen.copy()
116
-        self.x = x
117
-        self.y = y
118
-        self.j = j  # Centering
119
-        self.w = w  # width
120
-        self.t = t  # text
121
-
122
-    def draw(self, cr, highlight=False):
123
-
124
-        try:
125
-            layout = self.layout
126
-        except AttributeError:
127
-            layout = PangoCairo.create_layout(cr)
128
-
129
-            # set font options
130
-            # see http://lists.freedesktop.org/archives/cairo/2007-February/009688.html
131
-            context = layout.get_context()
132
-            fo = cairo.FontOptions()
133
-            fo.set_antialias(cairo.ANTIALIAS_DEFAULT)
134
-            fo.set_hint_style(cairo.HINT_STYLE_NONE)
135
-            fo.set_hint_metrics(cairo.HINT_METRICS_OFF)
136
-            try:
137
-                PangoCairo.context_set_font_options(context, fo)
138
-            except TypeError:
139
-                # XXX: Some broken pangocairo bindings show the error
140
-                # 'TypeError: font_options must be a cairo.FontOptions or None'
141
-                pass
142
-            except KeyError:
143
-                # cairo.FontOptions is not registered as a foreign struct in older PyGObject versions.
144
-                # https://git.gnome.org/browse/pygobject/commit/?id=b21f66d2a399b8c9a36a1758107b7bdff0ec8eaa
145
-                pass
146
-
147
-            # set font
148
-            font = Pango.FontDescription()
149
-
150
-            # https://developer.gnome.org/pango/stable/PangoMarkupFormat.html
151
-            markup = GObject.markup_escape_text(self.t)
152
-            if self.pen.bold:
153
-                markup = '<b>' + markup + '</b>'
154
-            if self.pen.italic:
155
-                markup = '<i>' + markup + '</i>'
156
-            if self.pen.underline:
157
-                markup = '<span underline="single">' + markup + '</span>'
158
-            if self.pen.strikethrough:
159
-                markup = '<s>' + markup + '</s>'
160
-            if self.pen.superscript:
161
-                markup = '<sup><small>' + markup + '</small></sup>'
162
-            if self.pen.subscript:
163
-                markup = '<sub><small>' + markup + '</small></sub>'
164
-
165
-            success, attrs, text, accel_char = Pango.parse_markup(markup, -1, '\x00')
166
-            assert success
167
-            layout.set_attributes(attrs)
168
-
169
-            font.set_family(self.pen.fontname)
170
-            font.set_absolute_size(self.pen.fontsize*Pango.SCALE)
171
-            layout.set_font_description(font)
172
-
173
-            # set text
174
-            layout.set_text(text, -1)
175
-
176
-            # cache it
177
-            self.layout = layout
178
-        else:
179
-            PangoCairo.update_layout(cr, layout)
180
-
181
-        descent = 2 # XXX get descender from font metrics
182
-
183
-        width, height = layout.get_size()
184
-        width = float(width)/Pango.SCALE
185
-        height = float(height)/Pango.SCALE
186
-
187
-        # we know the width that dot thinks this text should have
188
-        # we do not necessarily have a font with the same metrics
189
-        # scale it so that the text fits inside its box
190
-        if width > self.w:
191
-            f = self.w / width
192
-            width = self.w # equivalent to width *= f
193
-            height *= f
194
-            descent *= f
195
-        else:
196
-            f = 1.0
197
-
198
-        if self.j == self.LEFT:
199
-            x = self.x
200
-        elif self.j == self.CENTER:
201
-            x = self.x - 0.5*width
202
-        elif self.j == self.RIGHT:
203
-            x = self.x - width
204
-        else:
205
-            assert 0
206
-
207
-        y = self.y - height + descent
208
-
209
-        cr.move_to(x, y)
210
-
211
-        cr.save()
212
-        cr.scale(f, f)
213
-        cr.set_source_rgba(*self.select_pen(highlight).color)
214
-        PangoCairo.show_layout(cr, layout)
215
-        cr.restore()
216
-
217
-        if 0: # DEBUG
218
-            # show where dot thinks the text should appear
219
-            cr.set_source_rgba(1, 0, 0, .9)
220
-            if self.j == self.LEFT:
221
-                x = self.x
222
-            elif self.j == self.CENTER:
223
-                x = self.x - 0.5*self.w
224
-            elif self.j == self.RIGHT:
225
-                x = self.x - self.w
226
-            cr.move_to(x, self.y)
227
-            cr.line_to(x+self.w, self.y)
228
-            cr.stroke()
229
-
230
-    def search_text(self, regexp):
231
-        return regexp.search(self.t) is not None
232
-
233
-
234
-class ImageShape(Shape):
235
-
236
-    def __init__(self, pen, x0, y0, w, h, path):
237
-        Shape.__init__(self)
238
-        self.pen = pen.copy()
239
-        self.x0 = x0
240
-        self.y0 = y0
241
-        self.w = w
242
-        self.h = h
243
-        self.path = path
244
-
245
-    def draw(self, cr, highlight=False):
246
-        pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.path)
247
-        sx = float(self.w)/float(pixbuf.get_width())
248
-        sy = float(self.h)/float(pixbuf.get_height())
249
-        cr.save()
250
-        cr.translate(self.x0, self.y0 - self.h)
251
-        cr.scale(sx, sy)
252
-        Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0)
253
-        cr.paint()
254
-        cr.restore()
255
-
256
-
257
-class EllipseShape(Shape):
258
-
259
-    def __init__(self, pen, x0, y0, w, h, filled=False):
260
-        Shape.__init__(self)
261
-        self.pen = pen.copy()
262
-        self.x0 = x0
263
-        self.y0 = y0
264
-        self.w = w
265
-        self.h = h
266
-        self.filled = filled
267
-
268
-    def draw(self, cr, highlight=False):
269
-        cr.save()
270
-        cr.translate(self.x0, self.y0)
271
-        cr.scale(self.w, self.h)
272
-        cr.move_to(1.0, 0.0)
273
-        cr.arc(0.0, 0.0, 1.0, 0, 2.0*math.pi)
274
-        cr.restore()
275
-        pen = self.select_pen(highlight)
276
-        if self.filled:
277
-            cr.set_source_rgba(*pen.fillcolor)
278
-            cr.fill()
279
-        else:
280
-            cr.set_dash(pen.dash)
281
-            cr.set_line_width(pen.linewidth)
282
-            cr.set_source_rgba(*pen.color)
283
-            cr.stroke()
284
-
285
-
286
-class PolygonShape(Shape):
287
-
288
-    def __init__(self, pen, points, filled=False):
289
-        Shape.__init__(self)
290
-        self.pen = pen.copy()
291
-        self.points = points
292
-        self.filled = filled
293
-
294
-    def draw(self, cr, highlight=False):
295
-        x0, y0 = self.points[-1]
296
-        cr.move_to(x0, y0)
297
-        for x, y in self.points:
298
-            cr.line_to(x, y)
299
-        cr.close_path()
300
-        pen = self.select_pen(highlight)
301
-        if self.filled:
302
-            cr.set_source_rgba(*pen.fillcolor)
303
-            cr.fill_preserve()
304
-            cr.fill()
305
-        else:
306
-            cr.set_dash(pen.dash)
307
-            cr.set_line_width(pen.linewidth)
308
-            cr.set_source_rgba(*pen.color)
309
-            cr.stroke()
310
-
311
-
312
-class LineShape(Shape):
313
-
314
-    def __init__(self, pen, points):
315
-        Shape.__init__(self)
316
-        self.pen = pen.copy()
317
-        self.points = points
318
-
319
-    def draw(self, cr, highlight=False):
320
-        x0, y0 = self.points[0]
321
-        cr.move_to(x0, y0)
322
-        for x1, y1 in self.points[1:]:
323
-            cr.line_to(x1, y1)
324
-        pen = self.select_pen(highlight)
325
-        cr.set_dash(pen.dash)
326
-        cr.set_line_width(pen.linewidth)
327
-        cr.set_source_rgba(*pen.color)
328
-        cr.stroke()
329
-
330
-
331
-class BezierShape(Shape):
332
-
333
-    def __init__(self, pen, points, filled=False):
334
-        Shape.__init__(self)
335
-        self.pen = pen.copy()
336
-        self.points = points
337
-        self.filled = filled
338
-
339
-    def draw(self, cr, highlight=False):
340
-        x0, y0 = self.points[0]
341
-        cr.move_to(x0, y0)
342
-        for i in range(1, len(self.points), 3):
343
-            x1, y1 = self.points[i]
344
-            x2, y2 = self.points[i + 1]
345
-            x3, y3 = self.points[i + 2]
346
-            cr.curve_to(x1, y1, x2, y2, x3, y3)
347
-        pen = self.select_pen(highlight)
348
-        if self.filled:
349
-            cr.set_source_rgba(*pen.fillcolor)
350
-            cr.fill_preserve()
351
-            cr.fill()
352
-        else:
353
-            cr.set_dash(pen.dash)
354
-            cr.set_line_width(pen.linewidth)
355
-            cr.set_source_rgba(*pen.color)
356
-            cr.stroke()
357
-
358
-
359
-class CompoundShape(Shape):
360
-
361
-    def __init__(self, shapes):
362
-        Shape.__init__(self)
363
-        self.shapes = shapes
364
-
365
-    def draw(self, cr, highlight=False):
366
-        for shape in self.shapes:
367
-            shape.draw(cr, highlight=highlight)
368
-
369
-    def search_text(self, regexp):
370
-        for shape in self.shapes:
371
-            if shape.search_text(regexp):
372
-                return True
373
-        return False
374
-
375
-
376
-class Url(object):
377
-
378
-    def __init__(self, item, url, highlight=None):
379
-        self.item = item
380
-        self.url = url
381
-        if highlight is None:
382
-            highlight = set([item])
383
-        self.highlight = highlight
384
-
385
-
386
-class Jump(object):
387
-
388
-    def __init__(self, item, x, y, highlight=None):
389
-        self.item = item
390
-        self.x = x
391
-        self.y = y
392
-        if highlight is None:
393
-            highlight = set([item])
394
-        self.highlight = highlight
395
-
396
-
397
-class Element(CompoundShape):
398
-    """Base class for graph nodes and edges."""
399
-
400
-    def __init__(self, shapes):
401
-        CompoundShape.__init__(self, shapes)
402
-
403
-    def is_inside(self, x, y):
404
-        return False
405
-
406
-    def get_url(self, x, y):
407
-        return None
408
-
409
-    def get_jump(self, x, y):
410
-        return None
411
-
412
-
413
-class Node(Element):
414
-
415
-    def __init__(self, id, x, y, w, h, shapes, url):
416
-        Element.__init__(self, shapes)
417
-
418
-        self.id = id
419
-        self.x = x
420
-        self.y = y
421
-
422
-        self.x1 = x - 0.5*w
423
-        self.y1 = y - 0.5*h
424
-        self.x2 = x + 0.5*w
425
-        self.y2 = y + 0.5*h
426
-
427
-        self.url = url
428
-
429
-    def is_inside(self, x, y):
430
-        return self.x1 <= x and x <= self.x2 and self.y1 <= y and y <= self.y2
431
-
432
-    def get_url(self, x, y):
433
-        if self.url is None:
434
-            return None
435
-        if self.is_inside(x, y):
436
-            return Url(self, self.url)
437
-        return None
438
-
439
-    def get_jump(self, x, y):
440
-        if self.is_inside(x, y):
441
-            return Jump(self, self.x, self.y)
442
-        return None
443
-
444
-    def __repr__(self):
445
-        return "<Node %s>" % self.id
446
-
447
-
448
-def square_distance(x1, y1, x2, y2):
449
-    deltax = x2 - x1
450
-    deltay = y2 - y1
451
-    return deltax*deltax + deltay*deltay
452
-
453
-
454
-class Edge(Element):
455
-
456
-    def __init__(self, src, dst, points, shapes):
457
-        Element.__init__(self, shapes)
458
-        self.src = src
459
-        self.dst = dst
460
-        self.points = points
461
-
462
-    RADIUS = 10
463
-
464
-    def is_inside_begin(self, x, y):
465
-        return square_distance(x, y, *self.points[0]) <= self.RADIUS*self.RADIUS
466
-
467
-    def is_inside_end(self, x, y):
468
-        return square_distance(x, y, *self.points[-1]) <= self.RADIUS*self.RADIUS
469
-
470
-    def is_inside(self, x, y):
471
-        if self.is_inside_begin(x, y):
472
-            return True
473
-        if self.is_inside_end(x, y):
474
-            return True
475
-        return False
476
-
477
-    def get_jump(self, x, y):
478
-        if self.is_inside_begin(x, y):
479
-            return Jump(self, self.dst.x, self.dst.y, highlight=set([self, self.dst]))
480
-        if self.is_inside_end(x, y):
481
-            return Jump(self, self.src.x, self.src.y, highlight=set([self, self.src]))
482
-        return None
483
-
484
-    def __repr__(self):
485
-        return "<Edge %s -> %s>" % (self.src, self.dst)
486
-
487
-
488
-class Graph(Shape):
489
-
490
-    def __init__(self, width=1, height=1, shapes=(), nodes=(), edges=()):
491
-        Shape.__init__(self)
492
-
493
-        self.width = width
494
-        self.height = height
495
-        self.shapes = shapes
496
-        self.nodes = nodes
497
-        self.edges = edges
498
-
499
-    def get_size(self):
500
-        return self.width, self.height
501
-
502
-    def draw(self, cr, highlight_items=None):
503
-        if highlight_items is None:
504
-            highlight_items = ()
505
-        cr.set_source_rgba(0.0, 0.0, 0.0, 1.0)
506
-
507
-        cr.set_line_cap(cairo.LINE_CAP_BUTT)
508
-        cr.set_line_join(cairo.LINE_JOIN_MITER)
509
-
510
-        for shape in self.shapes:
511
-            shape.draw(cr)
512
-        for edge in self.edges:
513
-            should_highlight = any(e in highlight_items
514
-                                   for e in (edge, edge.src, edge.dst))
515
-            edge.draw(cr, highlight=should_highlight)
516
-        for node in self.nodes:
517
-            node.draw(cr, highlight=(node in highlight_items))
518
-
519
-    def get_element(self, x, y):
520
-        for node in self.nodes:
521
-            if node.is_inside(x, y):
522
-                return node
523
-        for edge in self.edges:
524
-            if edge.is_inside(x, y):
525
-                return edge
526
-
527
-    def get_url(self, x, y):
528
-        for node in self.nodes:
529
-            url = node.get_url(x, y)
530
-            if url is not None:
531
-                return url
532
-        return None
533
-
534
-    def get_jump(self, x, y):
535
-        for edge in self.edges:
536
-            jump = edge.get_jump(x, y)
537
-            if jump is not None:
538
-                return jump
539
-        for node in self.nodes:
540
-            jump = node.get_jump(x, y)
541
-            if jump is not None:
542
-                return jump
543
-        return None
544
-
545
-
546
-BOLD = 1
547
-ITALIC = 2
548
-UNDERLINE = 4
549
-SUPERSCRIPT = 8
550
-SUBSCRIPT = 16
551
-STRIKE_THROUGH = 32
552
-OVERLINE = 64
553
-
554
-
555
-class XDotAttrParser:
556
-    """Parser for xdot drawing attributes.
557
-    See also:
558
-    - http://www.graphviz.org/doc/info/output.html#d:xdot
559
-    """
560
-
561
-    def __init__(self, parser, buf):
562
-        self.parser = parser
563
-        self.buf = buf
564
-        self.pos = 0
565
-        
566
-        self.pen = Pen()
567
-        self.shapes = []
568
-
569
-    def __bool__(self):
570
-        return self.pos < len(self.buf)
571
-
572
-    def read_code(self):
573
-        pos = self.buf.find(b" ", self.pos)
574
-        res = self.buf[self.pos:pos]
575
-        self.pos = pos + 1
576
-        self.skip_space()
577
-        res = res.decode('utf-8')
578
-        return res
579
-
580
-    def skip_space(self):
581
-        while self.pos < len(self.buf) and self.buf[self.pos : self.pos + 1].isspace():
582
-            self.pos += 1
583
-
584
-    def read_int(self):
585
-        return int(self.read_code())
586
-
587
-    def read_float(self):
588
-        return float(self.read_code())
589
-
590
-    def read_point(self):
591
-        x = self.read_float()
592
-        y = self.read_float()
593
-        return self.transform(x, y)
594
-
595
-    def read_text(self):
596
-        num = self.read_int()
597
-        pos = self.buf.find(b"-", self.pos) + 1
598
-        self.pos = pos + num
599
-        res = self.buf[pos:self.pos]
600
-        self.skip_space()
601
-        res = res.decode('utf-8')
602
-        return res
603
-
604
-    def read_polygon(self):
605
-        n = self.read_int()
606
-        p = []
607
-        for i in range(n):
608
-            x, y = self.read_point()
609
-            p.append((x, y))
610
-        return p
611
-
612
-    def read_color(self):
613
-        # See http://www.graphviz.org/doc/info/attrs.html#k:color
614
-        c = self.read_text()
615
-        c1 = c[:1]
616
-        if c1 == '#':
617
-            hex2float = lambda h: float(int(h, 16)/255.0)
618
-            r = hex2float(c[1:3])
619
-            g = hex2float(c[3:5])
620
-            b = hex2float(c[5:7])
621
-            try:
622
-                a = hex2float(c[7:9])
623
-            except (IndexError, ValueError):
624
-                a = 1.0
625
-            return r, g, b, a
626
-        elif c1.isdigit() or c1 == ".":
627
-            # "H,S,V" or "H S V" or "H, S, V" or any other variation
628
-            h, s, v = map(float, c.replace(",", " ").split())
629
-            r, g, b = colorsys.hsv_to_rgb(h, s, v)
630
-            a = 1.0
631
-            return r, g, b, a
632
-        elif c1 == "[" or c1 == "(":
633
-            sys.stderr.write('warning: color gradients not supported yet\n')
634
-            return None
635
-        else:
636
-            return self.lookup_color(c)
637
-
638
-    def lookup_color(self, c):
639
-        try:
640
-            color = Gdk.color_parse(c)
641
-        except ValueError:
642
-            pass
643
-        else:
644
-            s = 1.0/65535.0
645
-            r = color.red*s
646
-            g = color.green*s
647
-            b = color.blue*s
648
-            a = 1.0
649
-            return r, g, b, a
650
-
651
-        try:
652
-            dummy, scheme, index = c.split('/')
653
-            r, g, b = brewer_colors[scheme][int(index)]
654
-        except (ValueError, KeyError):
655
-            pass
656
-        else:
657
-            s = 1.0/255.0
658
-            r = r*s
659
-            g = g*s
660
-            b = b*s
661
-            a = 1.0
662
-            return r, g, b, a
663
-                
664
-        sys.stderr.write("warning: unknown color '%s'\n" % c)
665
-        return None
666
-
667
-    def parse(self):
668
-        s = self
669
-
670
-        while s:
671
-            op = s.read_code()
672
-            if op == "c":
673
-                color = s.read_color()
674
-                if color is not None:
675
-                    self.handle_color(color, filled=False)
676
-            elif op == "C":
677
-                color = s.read_color()
678
-                if color is not None:
679
-                    self.handle_color(color, filled=True)
680
-            elif op == "S":
681
-                # http://www.graphviz.org/doc/info/attrs.html#k:style
682
-                style = s.read_text()
683
-                if style.startswith("setlinewidth("):
684
-                    lw = style.split("(")[1].split(")")[0]
685
-                    lw = float(lw)
686
-                    self.handle_linewidth(lw)
687
-                elif style in ("solid", "dashed", "dotted"):
688
-                    self.handle_linestyle(style)
689
-            elif op == "F":
690
-                size = s.read_float()
691
-                name = s.read_text()
692
-                self.handle_font(size, name)
693
-            elif op == "T":
694
-                x, y = s.read_point()
695
-                j = s.read_int()
696
-                w = s.read_float()
697
-                t = s.read_text()
698
-                self.handle_text(x, y, j, w, t)
699
-            elif op == "t":
700
-                f = s.read_int()
701
-                self.handle_font_characteristics(f)
702
-            elif op == "E":
703
-                x0, y0 = s.read_point()
704
-                w = s.read_float()
705
-                h = s.read_float()
706
-                self.handle_ellipse(x0, y0, w, h, filled=True)
707
-            elif op == "e":
708
-                x0, y0 = s.read_point()
709
-                w = s.read_float()
710
-                h = s.read_float()
711
-                self.handle_ellipse(x0, y0, w, h, filled=False)
712
-            elif op == "L":
713
-                points = self.read_polygon()
714
-                self.handle_line(points)
715
-            elif op == "B":
716
-                points = self.read_polygon()
717
-                self.handle_bezier(points, filled=False)
718
-            elif op == "b":
719
-                points = self.read_polygon()
720
-                self.handle_bezier(points, filled=True)
721
-            elif op == "P":
722
-                points = self.read_polygon()
723
-                self.handle_polygon(points, filled=True)
724
-            elif op == "p":
725
-                points = self.read_polygon()
726
-                self.handle_polygon(points, filled=False)
727
-            elif op == "I":
728
-                x0, y0 = s.read_point()
729
-                w = s.read_float()
730
-                h = s.read_float()
731
-                path = s.read_text()
732
-                self.handle_image(x0, y0, w, h, path)
733
-            else:
734
-                sys.stderr.write("error: unknown xdot opcode '%s'\n" % op)
735
-                sys.exit(1)
736
-
737
-        return self.shapes
738
-    
739
-    def transform(self, x, y):
740
-        return self.parser.transform(x, y)
741
-
742
-    def handle_color(self, color, filled=False):
743
-        if filled:
744
-            self.pen.fillcolor = color
745
-        else:
746
-            self.pen.color = color
747
-
748
-    def handle_linewidth(self, linewidth):
749
-        self.pen.linewidth = linewidth
750
-
751
-    def handle_linestyle(self, style):
752
-        if style == "solid":
753
-            self.pen.dash = ()
754
-        elif style == "dashed":
755
-            self.pen.dash = (6, )       # 6pt on, 6pt off
756
-        elif style == "dotted":
757
-            self.pen.dash = (2, 4)       # 2pt on, 4pt off
758
-
759
-    def handle_font(self, size, name):
760
-        self.pen.fontsize = size
761
-        self.pen.fontname = name
762
-
763
-    def handle_font_characteristics(self, flags):
764
-        self.pen.bold = bool(flags & BOLD)
765
-        self.pen.italic = bool(flags & ITALIC)
766
-        self.pen.underline = bool(flags & UNDERLINE)
767
-        self.pen.superscript = bool(flags & SUPERSCRIPT)
768
-        self.pen.subscript = bool(flags & SUBSCRIPT)
769
-        self.pen.strikethrough = bool(flags & STRIKE_THROUGH)
770
-        self.pen.overline = bool(flags & OVERLINE)
771
-        if self.pen.overline:
772
-            sys.stderr.write('warning: overlined text not supported yet\n')
773
-
774
-    def handle_text(self, x, y, j, w, t):
775
-        self.shapes.append(TextShape(self.pen, x, y, j, w, t))
776
-
777
-    def handle_ellipse(self, x0, y0, w, h, filled=False):
778
-        if filled:
779
-            # xdot uses this to mean "draw a filled shape with an outline"
780
-            self.shapes.append(EllipseShape(self.pen, x0, y0, w, h, filled=True))
781
-        self.shapes.append(EllipseShape(self.pen, x0, y0, w, h))
782
-
783
-    def handle_image(self, x0, y0, w, h, path):
784
-        self.shapes.append(ImageShape(self.pen, x0, y0, w, h, path))
785
-
786
-    def handle_line(self, points):
787
-        self.shapes.append(LineShape(self.pen, points))
788
-
789
-    def handle_bezier(self, points, filled=False):
790
-        if filled:
791
-            # xdot uses this to mean "draw a filled shape with an outline"
792
-            self.shapes.append(BezierShape(self.pen, points, filled=True))
793
-        self.shapes.append(BezierShape(self.pen, points))
794
-
795
-    def handle_polygon(self, points, filled=False):
796
-        if filled:
797
-            # xdot uses this to mean "draw a filled shape with an outline"
798
-            self.shapes.append(PolygonShape(self.pen, points, filled=True))
799
-        self.shapes.append(PolygonShape(self.pen, points))
800
-
801
-
802
-EOF = -1
803
-SKIP = -2
804
-
805
-
806
-class ParseError(Exception):
807
-
808
-    def __init__(self, msg=None, filename=None, line=None, col=None):
809
-        self.msg = msg
810
-        self.filename = filename
811
-        self.line = line
812
-        self.col = col
813
-
814
-    def __str__(self):
815
-        return ':'.join([str(part) for part in (self.filename, self.line, self.col, self.msg) if part != None])
816
-        
817
-
818
-class Scanner:
819
-    """Stateless scanner."""
820
-
821
-    # should be overriden by derived classes
822
-    tokens = []
823
-    symbols = {}
824
-    literals = {}
825
-    ignorecase = False
826
-
827
-    def __init__(self):
828
-        flags = re.DOTALL
829
-        if self.ignorecase:
830
-            flags |= re.IGNORECASE
831
-        self.tokens_re = re.compile(
832
-            b'|'.join([b'(' + regexp + b')' for type, regexp, test_lit in self.tokens]),
833
-             flags
834
-        )
835
-
836
-    def next(self, buf, pos):
837
-        if pos >= len(buf):
838
-            return EOF, b'', pos
839
-        mo = self.tokens_re.match(buf, pos)
840
-        if mo:
841
-            text = mo.group()
842
-            type, regexp, test_lit = self.tokens[mo.lastindex - 1]
843
-            pos = mo.end()
844
-            if test_lit:
845
-                type = self.literals.get(text, type)
846
-            return type, text, pos
847
-        else:
848
-            c = buf[pos : pos + 1]
849
-            return self.symbols.get(c, None), c, pos + 1
850
-
851
-
852
-class Token:
853
-
854
-    def __init__(self, type, text, line, col):
855
-        self.type = type
856
-        self.text = text
857
-        self.line = line
858
-        self.col = col
859
-
860
-
861
-class Lexer:
862
-
863
-    # should be overriden by derived classes
864
-    scanner = None
865
-    tabsize = 8
866
-
867
-    newline_re = re.compile(br'\r\n?|\n')
868
-
869
-    def __init__(self, buf = None, pos = 0, filename = None, fp = None):
870
-        if fp is not None:
871
-            try:
872
-                fileno = fp.fileno()
873
-                length = os.path.getsize(fp.name)
874
-                import mmap
875
-            except:
876
-                # read whole file into memory
877
-                buf = fp.read()
878
-                pos = 0
879
-            else:
880
-                # map the whole file into memory
881
-                if length:
882
-                    # length must not be zero
883
-                    buf = mmap.mmap(fileno, length, access = mmap.ACCESS_READ)
884
-                    pos = os.lseek(fileno, 0, 1)
885
-                else:
886
-                    buf = b''
887
-                    pos = 0
888
-
889
-            if filename is None:
890
-                try:
891
-                    filename = fp.name
892
-                except AttributeError:
893
-                    filename = None
894
-
895
-        self.buf = buf
896
-        self.pos = pos
897
-        self.line = 1
898
-        self.col = 1
899
-        self.filename = filename
900
-
901
-    def __next__(self):
902
-        while True:
903
-            # save state
904
-            pos = self.pos
905
-            line = self.line
906
-            col = self.col
907
-
908
-            type, text, endpos = self.scanner.next(self.buf, pos)
909
-            assert isinstance(text, bytes)
910
-            assert pos + len(text) == endpos
911
-            self.consume(text)
912
-            type, text = self.filter(type, text)
913
-            self.pos = endpos
914
-
915
-            if type == SKIP:
916
-                continue
917
-            elif type is None:
918
-                msg = 'unexpected char %r' % (text,)
919
-                raise ParseError(msg, self.filename, line, col)
920
-            else:
921
-                break
922
-        return Token(type = type, text = text, line = line, col = col)
923
-
924
-    def consume(self, text):
925
-        # update line number
926
-        pos = 0
927
-        for mo in self.newline_re.finditer(text, pos):
928
-            self.line += 1
929
-            self.col = 1
930
-            pos = mo.end()
931
-
932
-        # update column number
933
-        while True:
934
-            tabpos = text.find(b'\t', pos)
935
-            if tabpos == -1:
936
-                break
937
-            self.col += tabpos - pos
938
-            self.col = ((self.col - 1)//self.tabsize + 1)*self.tabsize + 1
939
-            pos = tabpos + 1
940
-        self.col += len(text) - pos
941
-
942
-
943
-class Parser:
944
-
945
-    def __init__(self, lexer):
946
-        self.lexer = lexer
947
-        self.lookahead = next(self.lexer)
948
-
949
-    def match(self, type):
950
-        if self.lookahead.type != type:
951
-            raise ParseError(
952
-                msg = 'unexpected token %r' % self.lookahead.text, 
953
-                filename = self.lexer.filename, 
954
-                line = self.lookahead.line, 
955
-                col = self.lookahead.col)
956
-
957
-    def skip(self, type):
958
-        while self.lookahead.type != type:
959
-            if self.lookahead.type == EOF:
960
-                raise ParseError(
961
-                   msg = 'unexpected end of file',
962
-                   filename = self.lexer.filename,
963
-                   line = self.lookahead.line,
964
-                   col = self.lookahead.col)
965
-            self.consume()
966
-
967
-    def consume(self):
968
-        token = self.lookahead
969
-        self.lookahead = next(self.lexer)
970
-        return token
971
-
972
-
973
-ID = 0
974
-STR_ID = 1
975
-HTML_ID = 2
976
-EDGE_OP = 3
977
-
978
-LSQUARE = 4
979
-RSQUARE = 5
980
-LCURLY = 6
981
-RCURLY = 7
982
-COMMA = 8
983
-COLON = 9
984
-SEMI = 10
985
-EQUAL = 11
986
-PLUS = 12
987
-
988
-STRICT = 13
989
-GRAPH = 14
990
-DIGRAPH = 15
991
-NODE = 16
992
-EDGE = 17
993
-SUBGRAPH = 18
994
-
995
-
996
-class DotScanner(Scanner):
997
-
998
-    # token regular expression table
999
-    tokens = [
1000
-        # whitespace and comments
1001
-        (SKIP,
1002
-            br'[ \t\f\r\n\v]+|'
1003
-            br'//[^\r\n]*|'
1004
-            br'/\*.*?\*/|'
1005
-            br'#[^\r\n]*',
1006
-        False),
1007
-
1008
-        # Alphanumeric IDs
1009
-        (ID, br'[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*', True),
1010
-
1011
-        # Numeric IDs
1012
-        (ID, br'-?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)', False),
1013
-
1014
-        # String IDs
1015
-        (STR_ID, br'"[^"\\]*(?:\\.[^"\\]*)*"', False),
1016
-
1017
-        # HTML IDs
1018
-        (HTML_ID, br'<[^<>]*(?:<[^<>]*>[^<>]*)*>', False),
1019
-
1020
-        # Edge operators
1021
-        (EDGE_OP, br'-[>-]', False),
1022
-    ]
1023
-
1024
-    # symbol table
1025
-    symbols = {
1026
-        b'[': LSQUARE,
1027
-        b']': RSQUARE,
1028
-        b'{': LCURLY,
1029
-        b'}': RCURLY,
1030
-        b',': COMMA,
1031
-        b':': COLON,
1032
-        b';': SEMI,
1033
-        b'=': EQUAL,
1034
-        b'+': PLUS,
1035
-    }
1036
-
1037
-    # literal table
1038
-    literals = {
1039
-        b'strict': STRICT,
1040
-        b'graph': GRAPH,
1041
-        b'digraph': DIGRAPH,
1042
-        b'node': NODE,
1043
-        b'edge': EDGE,
1044
-        b'subgraph': SUBGRAPH,
1045
-    }
1046
-
1047
-    ignorecase = True
1048
-
1049
-
1050
-class DotLexer(Lexer):
1051
-
1052
-    scanner = DotScanner()
1053
-
1054
-    def filter(self, type, text):
1055
-        # TODO: handle charset
1056
-        if type == STR_ID:
1057
-            text = text[1:-1]
1058
-
1059
-            # line continuations
1060
-            text = text.replace(b'\\\r\n', b'')
1061
-            text = text.replace(b'\\\r', b'')
1062
-            text = text.replace(b'\\\n', b'')
1063
-            
1064
-            # quotes
1065
-            text = text.replace(b'\\"', b'"')
1066
-
1067
-            # layout engines recognize other escape codes (many non-standard)
1068
-            # but we don't translate them here
1069
-
1070
-            type = ID
1071
-
1072
-        elif type == HTML_ID:
1073
-            text = text[1:-1]
1074
-            type = ID
1075
-
1076
-        return type, text
1077
-
1078
-
1079
-class DotParser(Parser):
1080
-
1081
-    def __init__(self, lexer):
1082
-        Parser.__init__(self, lexer)
1083
-        self.graph_attrs = {}
1084
-        self.node_attrs = {}
1085
-        self.edge_attrs = {}
1086
-
1087
-    def parse(self):
1088
-        self.parse_graph()
1089
-        self.match(EOF)
1090
-
1091
-    def parse_graph(self):
1092
-        if self.lookahead.type == STRICT:
1093
-            self.consume()
1094
-        self.skip(LCURLY)
1095
-        self.consume()
1096
-        while self.lookahead.type != RCURLY:
1097
-            self.parse_stmt()
1098
-        self.consume()
1099
-
1100
-    def parse_subgraph(self):
1101
-        id = None
1102
-        if self.lookahead.type == SUBGRAPH:
1103
-            self.consume()
1104
-            if self.lookahead.type == ID:
1105
-                id = self.lookahead.text
1106
-                self.consume()
1107
-        if self.lookahead.type == LCURLY:
1108
-            self.consume()
1109
-            while self.lookahead.type != RCURLY:
1110
-                self.parse_stmt()
1111
-            self.consume()
1112
-        return id
1113
-
1114
-    def parse_stmt(self):
1115
-        if self.lookahead.type == GRAPH:
1116
-            self.consume()
1117
-            attrs = self.parse_attrs()
1118
-            self.graph_attrs.update(attrs)
1119
-            self.handle_graph(attrs)
1120
-        elif self.lookahead.type == NODE:
1121
-            self.consume()
1122
-            self.node_attrs.update(self.parse_attrs())
1123
-        elif self.lookahead.type == EDGE:
1124
-            self.consume()
1125
-            self.edge_attrs.update(self.parse_attrs())
1126
-        elif self.lookahead.type in (SUBGRAPH, LCURLY):
1127
-            self.parse_subgraph()
1128
-        else:
1129
-            id = self.parse_node_id()
1130
-            if self.lookahead.type == EDGE_OP:
1131
-                self.consume()
1132
-                node_ids = [id, self.parse_node_id()]
1133
-                while self.lookahead.type == EDGE_OP:
1134
-                    node_ids.append(self.parse_node_id())
1135
-                attrs = self.parse_attrs()
1136
-                for i in range(0, len(node_ids) - 1):
1137
-                    self.handle_edge(node_ids[i], node_ids[i + 1], attrs)
1138
-            elif self.lookahead.type == EQUAL:
1139
-                self.consume()
1140
-                self.parse_id()
1141
-            else:
1142
-                attrs = self.parse_attrs()
1143
-                self.handle_node(id, attrs)
1144
-        if self.lookahead.type == SEMI:
1145
-            self.consume()
1146
-
1147
-    def parse_attrs(self):
1148
-        attrs = {}
1149
-        while self.lookahead.type == LSQUARE:
1150
-            self.consume()
1151
-            while self.lookahead.type != RSQUARE:
1152
-                name, value = self.parse_attr()
1153
-                name = name.decode('utf-8')
1154
-                attrs[name] = value
1155
-                if self.lookahead.type == COMMA:
1156
-                    self.consume()
1157
-            self.consume()
1158
-        return attrs
1159
-
1160
-    def parse_attr(self):
1161
-        name = self.parse_id()
1162
-        if self.lookahead.type == EQUAL:
1163
-            self.consume()
1164
-            value = self.parse_id()
1165
-        else:
1166
-            value = b'true'
1167
-        return name, value
1168
-
1169
-    def parse_node_id(self):
1170
-        node_id = self.parse_id()
1171
-        if self.lookahead.type == COLON:
1172
-            self.consume()
1173
-            port = self.parse_id()
1174
-            if self.lookahead.type == COLON:
1175
-                self.consume()
1176
-                compass_pt = self.parse_id()
1177
-            else:
1178
-                compass_pt = None
1179
-        else:
1180
-            port = None
1181
-            compass_pt = None
1182
-        # XXX: we don't really care about port and compass point values when parsing xdot
1183
-        return node_id
1184
-
1185
-    def parse_id(self):
1186
-        self.match(ID)
1187
-        id = self.lookahead.text
1188
-        self.consume()
1189
-        return id
1190
-
1191
-    def handle_graph(self, attrs):
1192
-        pass
1193
-
1194
-    def handle_node(self, id, attrs):
1195
-        pass
1196
-
1197
-    def handle_edge(self, src_id, dst_id, attrs):
1198
-        pass
1199
-
1200
-
1201
-class XDotParser(DotParser):
1202
-
1203
-    XDOTVERSION = '1.7'
1204
-
1205
-    def __init__(self, xdotcode):
1206
-        lexer = DotLexer(buf = xdotcode)
1207
-        DotParser.__init__(self, lexer)
1208
-        
1209
-        self.nodes = []
1210
-        self.edges = []
1211
-        self.shapes = []
1212
-        self.node_by_name = {}
1213
-        self.top_graph = True
1214
-        self.width = 0
1215
-        self.height = 0
1216
-
1217
-    def handle_graph(self, attrs):
1218
-        if self.top_graph:
1219
-            # Check xdot version
1220
-            try:
1221
-                xdotversion = attrs['xdotversion']
1222
-            except KeyError:
1223
-                pass
1224
-            else:
1225
-                if float(xdotversion) > float(self.XDOTVERSION):
1226
-                    sys.stderr.write('warning: xdot version %s, but supported is %s\n' % (xdotversion, self.XDOTVERSION))
1227
-
1228
-            # Parse bounding box
1229
-            try:
1230
-                bb = attrs['bb']
1231
-            except KeyError:
1232
-                return
1233
-
1234
-            if bb:
1235
-                xmin, ymin, xmax, ymax = map(float, bb.split(b","))
1236
-
1237
-                self.xoffset = -xmin
1238
-                self.yoffset = -ymax
1239
-                self.xscale = 1.0
1240
-                self.yscale = -1.0
1241
-                # FIXME: scale from points to pixels
1242
-
1243
-                self.width  = max(xmax - xmin, 1)
1244
-                self.height = max(ymax - ymin, 1)
1245
-
1246
-                self.top_graph = False
1247
-        
1248
-        for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
1249
-            if attr in attrs:
1250
-                parser = XDotAttrParser(self, attrs[attr])
1251
-                self.shapes.extend(parser.parse())
1252
-
1253
-    def handle_node(self, id, attrs):
1254
-        try:
1255
-            pos = attrs['pos']
1256
-        except KeyError:
1257
-            return
1258
-
1259
-        x, y = self.parse_node_pos(pos)
1260
-        w = float(attrs.get('width', 0))*72
1261
-        h = float(attrs.get('height', 0))*72
1262
-        shapes = []
1263
-        for attr in ("_draw_", "_ldraw_"):
1264
-            if attr in attrs:
1265
-                parser = XDotAttrParser(self, attrs[attr])
1266
-                shapes.extend(parser.parse())
1267
-        url = attrs.get('URL', None)
1268
-        node = Node(id, x, y, w, h, shapes, url)
1269
-        self.node_by_name[id] = node
1270
-        if shapes:
1271
-            self.nodes.append(node)
1272
-
1273
-    def handle_edge(self, src_id, dst_id, attrs):
1274
-        try:
1275
-            pos = attrs['pos']
1276
-        except KeyError:
1277
-            return
1278
-        
1279
-        points = self.parse_edge_pos(pos)
1280
-        shapes = []
1281
-        for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
1282
-            if attr in attrs:
1283
-                parser = XDotAttrParser(self, attrs[attr])
1284
-                shapes.extend(parser.parse())
1285
-        if shapes:
1286
-            src = self.node_by_name[src_id]
1287
-            dst = self.node_by_name[dst_id]
1288
-            self.edges.append(Edge(src, dst, points, shapes))
1289
-
1290
-    def parse(self):
1291
-        DotParser.parse(self)
1292
-        return Graph(self.width, self.height, self.shapes, self.nodes, self.edges)
1293
-
1294
-    def parse_node_pos(self, pos):
1295
-        x, y = pos.split(b",")
1296
-        return self.transform(float(x), float(y))
1297
-
1298
-    def parse_edge_pos(self, pos):
1299
-        points = []
1300
-        for entry in pos.split(b' '):
1301
-            fields = entry.split(b',')
1302
-            try:
1303
-                x, y = fields
1304
-            except ValueError:
1305
-                # TODO: handle start/end points
1306
-                continue
1307
-            else:
1308
-                points.append(self.transform(float(x), float(y)))
1309
-        return points
1310
-
1311
-    def transform(self, x, y):
1312
-        # XXX: this is not the right place for this code
1313
-        x = (x + self.xoffset)*self.xscale
1314
-        y = (y + self.yoffset)*self.yscale
1315
-        return x, y
1316
-
1317
-
1318
-class Animation(object):
1319
-
1320
-    step = 0.03 # seconds
1321
-
1322
-    def __init__(self, dot_widget):
1323
-        self.dot_widget = dot_widget
1324
-        self.timeout_id = None
1325
-
1326
-    def start(self):
1327
-        self.timeout_id = GLib.timeout_add(int(self.step * 1000), self.__real_tick)
1328
-
1329
-    def stop(self):
1330
-        self.dot_widget.animation = NoAnimation(self.dot_widget)
1331
-        if self.timeout_id is not None:
1332
-            GLib.source_remove(self.timeout_id)
1333
-            self.timeout_id = None
1334
-
1335
-    def __real_tick(self):
1336
-        try:
1337
-            if not self.tick():
1338
-                self.stop()
1339
-                return False
1340
-        except e:
1341
-            self.stop()
1342
-            raise e
1343
-        return True
1344
-
1345
-    def tick(self):
1346
-        return False
1347
-
1348
-
1349
-class NoAnimation(Animation):
1350
-
1351
-    def start(self):
1352
-        pass
1353
-
1354
-    def stop(self):
1355
-        pass
1356
-
1357
-
1358
-class LinearAnimation(Animation):
1359
-
1360
-    duration = 0.6
1361
-
1362
-    def start(self):
1363
-        self.started = time.time()
1364
-        Animation.start(self)
1365
-
1366
-    def tick(self):
1367
-        t = (time.time() - self.started) / self.duration
1368
-        self.animate(max(0, min(t, 1)))
1369
-        return (t < 1)
1370
-
1371
-    def animate(self, t):
1372
-        pass
1373
-
1374
-
1375
-class MoveToAnimation(LinearAnimation):
1376
-
1377
-    def __init__(self, dot_widget, target_x, target_y):
1378
-        Animation.__init__(self, dot_widget)
1379
-        self.source_x = dot_widget.x
1380
-        self.source_y = dot_widget.y
1381
-        self.target_x = target_x
1382
-        self.target_y = target_y
1383
-
1384
-    def animate(self, t):
1385
-        sx, sy = self.source_x, self.source_y
1386
-        tx, ty = self.target_x, self.target_y
1387
-        self.dot_widget.x = tx * t + sx * (1-t)
1388
-        self.dot_widget.y = ty * t + sy * (1-t)
1389
-        self.dot_widget.queue_draw()
1390
-
1391
-
1392
-class ZoomToAnimation(MoveToAnimation):
1393
-
1394
-    def __init__(self, dot_widget, target_x, target_y):
1395
-        MoveToAnimation.__init__(self, dot_widget, target_x, target_y)
1396
-        self.source_zoom = dot_widget.zoom_ratio
1397
-        self.target_zoom = self.source_zoom
1398
-        self.extra_zoom = 0
1399
-
1400
-        middle_zoom = 0.5 * (self.source_zoom + self.target_zoom)
1401
-
1402
-        distance = math.hypot(self.source_x - self.target_x,
1403
-                              self.source_y - self.target_y)
1404
-        rect = self.dot_widget.get_allocation()
1405
-        visible = min(rect.width, rect.height) / self.dot_widget.zoom_ratio
1406
-        visible *= 0.9
1407
-        if distance > 0:
1408
-            desired_middle_zoom = visible / distance
1409
-            self.extra_zoom = min(0, 4 * (desired_middle_zoom - middle_zoom))
1410
-
1411
-    def animate(self, t):
1412
-        a, b, c = self.source_zoom, self.extra_zoom, self.target_zoom
1413
-        self.dot_widget.zoom_ratio = c*t + b*t*(1-t) + a*(1-t)
1414
-        self.dot_widget.zoom_to_fit_on_resize = False
1415
-        MoveToAnimation.animate(self, t)
1416
-
1417
-
1418
-class DragAction(object):
1419
-
1420
-    def __init__(self, dot_widget):
1421
-        self.dot_widget = dot_widget
1422
-
1423
-    def on_button_press(self, event):
1424
-        self.startmousex = self.prevmousex = event.x
1425
-        self.startmousey = self.prevmousey = event.y
1426
-        self.start()
1427
-
1428
-    def on_motion_notify(self, event):
1429
-        if event.is_hint:
1430
-            window, x, y, state = event.window.get_device_position(event.device)
1431
-        else:
1432
-            x, y, state = event.x, event.y, event.state
1433
-        deltax = self.prevmousex - x
1434
-        deltay = self.prevmousey - y
1435
-        self.drag(deltax, deltay)
1436
-        self.prevmousex = x
1437
-        self.prevmousey = y
1438
-
1439
-    def on_button_release(self, event):
1440
-        self.stopmousex = event.x
1441
-        self.stopmousey = event.y
1442
-        self.stop()
1443
-
1444
-    def draw(self, cr):
1445
-        pass
1446
-
1447
-    def start(self):
1448
-        pass
1449
-
1450
-    def drag(self, deltax, deltay):
1451
-        pass
1452
-
1453
-    def stop(self):
1454
-        pass
1455
-
1456
-    def abort(self):
1457
-        pass
1458
-
1459
-
1460
-class NullAction(DragAction):
1461
-
1462
-    def on_motion_notify(self, event):
1463
-        if event.is_hint:
1464
-            window, x, y, state = event.window.get_device_position(event.device)
1465
-        else:
1466
-            x, y, state = event.x, event.y, event.state
1467
-        dot_widget = self.dot_widget
1468
-        item = dot_widget.get_url(x, y)
1469
-        if item is None:
1470
-            item = dot_widget.get_jump(x, y)
1471
-        if item is not None:
1472
-            dot_widget.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2))
1473
-            dot_widget.set_highlight(item.highlight)
1474
-        else:
1475
-            dot_widget.get_window().set_cursor(None)
1476
-            dot_widget.set_highlight(None)
1477
-
1478
-
1479
-class PanAction(DragAction):
1480
-
1481
-    def start(self):
1482
-        self.dot_widget.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.FLEUR))
1483
-
1484
-    def drag(self, deltax, deltay):
1485
-        self.dot_widget.x += deltax / self.dot_widget.zoom_ratio
1486
-        self.dot_widget.y += deltay / self.dot_widget.zoom_ratio
1487
-        self.dot_widget.queue_draw()
1488
-
1489
-    def stop(self):
1490
-        self.dot_widget.get_window().set_cursor(None)
1491
-
1492
-    abort = stop
1493
-
1494
-
1495
-class ZoomAction(DragAction):
1496
-
1497
-    def drag(self, deltax, deltay):
1498
-        self.dot_widget.zoom_ratio *= 1.005 ** (deltax + deltay)
1499
-        self.dot_widget.zoom_to_fit_on_resize = False
1500
-        self.dot_widget.queue_draw()
1501
-
1502
-    def stop(self):
1503
-        self.dot_widget.queue_draw()
1504
-
1505
-
1506
-class ZoomAreaAction(DragAction):
1507
-
1508
-    def drag(self, deltax, deltay):
1509
-        self.dot_widget.queue_draw()
1510
-
1511
-    def draw(self, cr):
1512
-        cr.save()
1513
-        cr.set_source_rgba(.5, .5, 1.0, 0.25)
1514
-        cr.rectangle(self.startmousex, self.startmousey,
1515
-                     self.prevmousex - self.startmousex,
1516
-                     self.prevmousey - self.startmousey)
1517
-        cr.fill()
1518
-        cr.set_source_rgba(.5, .5, 1.0, 1.0)
1519
-        cr.set_line_width(1)
1520
-        cr.rectangle(self.startmousex - .5, self.startmousey - .5,
1521
-                     self.prevmousex - self.startmousex + 1,
1522
-                     self.prevmousey - self.startmousey + 1)
1523
-        cr.stroke()
1524
-        cr.restore()
1525
-
1526
-    def stop(self):
1527
-        x1, y1 = self.dot_widget.window2graph(self.startmousex,
1528
-                                              self.startmousey)
1529
-        x2, y2 = self.dot_widget.window2graph(self.stopmousex,
1530
-                                              self.stopmousey)
1531
-        self.dot_widget.zoom_to_area(x1, y1, x2, y2)
1532
-
1533
-    def abort(self):
1534
-        self.dot_widget.queue_draw()
1535
-
1536
-
1537
-class DotWidget(Gtk.DrawingArea):
1538
-    """GTK widget that draws dot graphs."""
1539
-
1540
-    #TODO GTK3: Second argument has to be of type Gdk.EventButton instead of object.
1541
-    __gsignals__ = {
1542
-        'clicked' : (GObject.SIGNAL_RUN_LAST, None, (str, object)),
1543
-        'error' : (GObject.SIGNAL_RUN_LAST, None, (str,))
1544
-    }
1545
-
1546
-    filter = 'dot'
1547
-
1548
-    def __init__(self):
1549
-        Gtk.DrawingArea.__init__(self)
1550
-
1551
-        self.graph = Graph()
1552
-        self.openfilename = None
1553
-
1554
-        self.set_can_focus(True)
1555
-
1556
-        self.connect("draw", self.on_draw)
1557
-        self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK)
1558
-        self.connect("button-press-event", self.on_area_button_press)
1559
-        self.connect("button-release-event", self.on_area_button_release)
1560
-        self.add_events(Gdk.EventMask.POINTER_MOTION_MASK | 
1561
-                        Gdk.EventMask.POINTER_MOTION_HINT_MASK | 
1562
-                        Gdk.EventMask.BUTTON_RELEASE_MASK | 
1563
-                        Gdk.EventMask.SCROLL_MASK)
1564
-        self.connect("motion-notify-event", self.on_area_motion_notify)
1565
-        self.connect("scroll-event", self.on_area_scroll_event)
1566
-        self.connect("size-allocate", self.on_area_size_allocate)
1567
-
1568
-        self.connect('key-press-event', self.on_key_press_event)
1569
-        self.last_mtime = None
1570
-
1571
-        GLib.timeout_add(1000, self.update)
1572
-
1573
-        self.x, self.y = 0.0, 0.0
1574
-        self.zoom_ratio = 1.0
1575
-        self.zoom_to_fit_on_resize = False
1576
-        self.animation = NoAnimation(self)
1577
-        self.drag_action = NullAction(self)
1578
-        self.presstime = None
1579
-        self.highlight = None
1580
-        self.highlight_search = False
1581
-
1582
-    def error_dialog(self, message):
1583
-        self.emit('error', message)
1584
-
1585
-    def set_filter(self, filter):
1586
-        self.filter = filter
1587
-
1588
-    def run_filter(self, dotcode):
1589
-        if not self.filter:
1590
-            return dotcode
1591
-        try:
1592
-            p = subprocess.Popen(
1593
-                [self.filter, '-Txdot'],
1594
-                stdin=subprocess.PIPE,
1595
-                stdout=subprocess.PIPE,
1596
-                stderr=subprocess.PIPE,
1597
-                shell=False,
1598
-                universal_newlines=False
1599
-            )
1600
-        except OSError as exc:
1601
-            error = '%s: %s' % (self.filter, exc.strerror)
1602
-            p = subprocess.CalledProcessError(exc.errno, self.filter, exc.strerror)
1603
-        else:
1604
-            xdotcode, error = p.communicate(dotcode)
1605
-        error = error.rstrip()
1606
-        if error:
1607
-            error = error.decode()
1608
-            sys.stderr.write(error + '\n')
1609
-        if p.returncode != 0:
1610
-            self.error_dialog(error)
1611
-            return None
1612
-        return xdotcode
1613
-
1614
-    def set_dotcode(self, dotcode, filename=None):
1615
-        self.openfilename = None
1616
-        if isinstance(dotcode, str):
1617
-            dotcode = dotcode.encode('utf-8')
1618
-        xdotcode = self.run_filter(dotcode)
1619
-        if xdotcode is None:
1620
-            return False
1621
-        try:
1622
-            self.set_xdotcode(xdotcode)
1623
-        except ParseError as ex:
1624
-            self.error_dialog(str(ex))
1625
-            return False
1626
-        else:
1627
-            if filename is None:
1628
-                self.last_mtime = None
1629
-            else:
1630
-                self.last_mtime = os.stat(filename).st_mtime
1631
-            self.openfilename = filename
1632
-            return True
1633
-
1634
-    def set_xdotcode(self, xdotcode):
1635
-        assert isinstance(xdotcode, bytes)
1636
-        parser = XDotParser(xdotcode)
1637
-        self.graph = parser.parse()
1638
-        self.zoom_image(self.zoom_ratio, center=True)
1639
-
1640
-    def reload(self):
1641
-        if self.openfilename is not None:
1642
-            try:
1643
-                fp = open(self.openfilename, 'rt')
1644
-                self.set_dotcode(fp.read(), self.openfilename)
1645
-                fp.close()
1646
-            except IOError:
1647
-                pass
1648
-
1649
-    def update(self):
1650
-        if self.openfilename is not None:
1651
-            current_mtime = os.stat(self.openfilename).st_mtime
1652
-            if current_mtime != self.last_mtime:
1653
-                self.last_mtime = current_mtime
1654
-                self.reload()
1655
-        return True
1656
-
1657
-    def on_draw(self, widget, cr):
1658
-        cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
1659
-        cr.paint()
1660
-
1661
-        cr.save()
1662
-        rect = self.get_allocation()
1663
-        cr.translate(0.5*rect.width, 0.5*rect.height)
1664
-        cr.scale(self.zoom_ratio, self.zoom_ratio)
1665
-        cr.translate(-self.x, -self.y)
1666
-
1667
-        self.graph.draw(cr, highlight_items=self.highlight)
1668
-        cr.restore()
1669
-
1670
-        self.drag_action.draw(cr)
1671
-
1672
-        return False
1673
-
1674
-    def get_current_pos(self):
1675
-        return self.x, self.y
1676
-
1677
-    def set_current_pos(self, x, y):
1678
-        self.x = x
1679
-        self.y = y
1680
-        self.queue_draw()
1681
-
1682
-    def set_highlight(self, items, search=False):
1683
-        # Enable or disable search highlight
1684
-        if search:
1685
-            self.highlight_search = items is not None
1686
-        # Ignore cursor highlight while searching
1687
-        if self.highlight_search and not search:
1688
-            return
1689
-        if self.highlight != items:
1690
-            self.highlight = items
1691
-            self.queue_draw()
1692
-
1693
-    def zoom_image(self, zoom_ratio, center=False, pos=None):
1694
-        # Constrain zoom ratio to a sane range to prevent numeric instability.
1695
-        zoom_ratio = min(zoom_ratio, 1E4)
1696
-        zoom_ratio = max(zoom_ratio, 1E-6)
1697
-
1698
-        if center:
1699
-            self.x = self.graph.width/2
1700
-            self.y = self.graph.height/2
1701
-        elif pos is not None:
1702
-            rect = self.get_allocation()
1703
-            x, y = pos
1704
-            x -= 0.5*rect.width
1705
-            y -= 0.5*rect.height
1706
-            self.x += x / self.zoom_ratio - x / zoom_ratio
1707
-            self.y += y / self.zoom_ratio - y / zoom_ratio
1708
-        self.zoom_ratio = zoom_ratio
1709
-        self.zoom_to_fit_on_resize = False
1710
-        self.queue_draw()
1711
-
1712
-    def zoom_to_area(self, x1, y1, x2, y2):
1713
-        rect = self.get_allocation()
1714
-        width = abs(x1 - x2)
1715
-        height = abs(y1 - y2)
1716
-        if width == 0 and height == 0:
1717
-            self.zoom_ratio *= self.ZOOM_INCREMENT
1718
-        else:
1719
-            self.zoom_ratio = min(
1720
-                float(rect.width)/float(width),
1721
-                float(rect.height)/float(height)
1722
-            )
1723
-        self.zoom_to_fit_on_resize = False
1724
-        self.x = (x1 + x2) / 2
1725
-        self.y = (y1 + y2) / 2
1726
-        self.queue_draw()
1727
-
1728
-    def zoom_to_fit(self):
1729
-        rect = self.get_allocation()
1730
-        rect.x += self.ZOOM_TO_FIT_MARGIN
1731
-        rect.y += self.ZOOM_TO_FIT_MARGIN
1732
-        rect.width -= 2 * self.ZOOM_TO_FIT_MARGIN
1733
-        rect.height -= 2 * self.ZOOM_TO_FIT_MARGIN
1734
-        zoom_ratio = min(
1735
-            float(rect.width)/float(self.graph.width),
1736
-            float(rect.height)/float(self.graph.height)
1737
-        )
1738
-        self.zoom_image(zoom_ratio, center=True)
1739
-        self.zoom_to_fit_on_resize = True
1740
-
1741
-    ZOOM_INCREMENT = 1.25
1742
-    ZOOM_TO_FIT_MARGIN = 12
1743
-
1744
-    def on_zoom_in(self, action):
1745
-        self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
1746
-
1747
-    def on_zoom_out(self, action):
1748
-        self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
1749
-
1750
-    def on_zoom_fit(self, action):
1751
-        self.zoom_to_fit()
1752
-
1753
-    def on_zoom_100(self, action):
1754
-        self.zoom_image(1.0)
1755
-
1756
-    POS_INCREMENT = 100
1757
-
1758
-    def on_key_press_event(self, widget, event):
1759
-        if event.keyval == Gdk.KEY_Left:
1760
-            self.x -= self.POS_INCREMENT/self.zoom_ratio
1761
-            self.queue_draw()
1762
-            return True
1763
-        if event.keyval == Gdk.KEY_Right:
1764
-            self.x += self.POS_INCREMENT/self.zoom_ratio
1765
-            self.queue_draw()
1766
-            return True
1767
-        if event.keyval == Gdk.KEY_Up:
1768
-            self.y -= self.POS_INCREMENT/self.zoom_ratio
1769
-            self.queue_draw()
1770
-            return True
1771
-        if event.keyval == Gdk.KEY_Down:
1772
-            self.y += self.POS_INCREMENT/self.zoom_ratio
1773
-            self.queue_draw()
1774
-            return True
1775
-        if event.keyval in (Gdk.KEY_Page_Up,
1776
-                            Gdk.KEY_plus,
1777
-                            Gdk.KEY_equal,
1778
-                            Gdk.KEY_KP_Add):
1779
-            self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
1780
-            self.queue_draw()
1781
-            return True
1782
-        if event.keyval in (Gdk.KEY_Page_Down,
1783
-                            Gdk.KEY_minus,
1784
-                            Gdk.KEY_KP_Subtract):
1785
-            self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
1786
-            self.queue_draw()
1787
-            return True
1788
-        if event.keyval == Gdk.KEY_Escape:
1789
-            self.drag_action.abort()
1790
-            self.drag_action = NullAction(self)
1791
-            return True
1792
-        if event.keyval == Gdk.KEY_r:
1793
-            self.reload()
1794
-            return True
1795
-        if event.keyval == Gdk.KEY_f:
1796
-            win = widget.get_toplevel()
1797
-            find_toolitem = win.uimanager.get_widget('/ToolBar/Find')
1798
-            textentry = find_toolitem.get_children()
1799
-            win.set_focus(textentry[0])
1800
-            return True
1801
-        if event.keyval == Gdk.KEY_q:
1802
-            Gtk.main_quit()
1803
-            return True
1804
-        if event.keyval == Gdk.KEY_p:
1805
-            self.on_print()
1806
-            return True
1807
-        return False
1808
-
1809
-    print_settings = None
1810
-    def on_print(self, action=None):
1811
-        print_op = Gtk.PrintOperation()
1812
-
1813
-        if self.print_settings != None:
1814
-            print_op.set_print_settings(self.print_settings)
1815
-
1816
-        print_op.connect("begin_print", self.begin_print)
1817
-        print_op.connect("draw_page", self.draw_page)
1818
-
1819
-        res = print_op.run(Gtk.PrintOperationAction.PRINT_DIALOG, self.get_toplevel())
1820
-        if res == Gtk.PrintOperationResult.APPLY:
1821
-            self.print_settings = print_op.get_print_settings()
1822
-
1823
-    def begin_print(self, operation, context):
1824
-        operation.set_n_pages(1)
1825
-        return True
1826
-
1827
-    def draw_page(self, operation, context, page_nr):
1828
-        cr = context.get_cairo_context()
1829
-
1830
-        rect = self.get_allocation()
1831
-        cr.translate(0.5*rect.width, 0.5*rect.height)
1832
-        cr.scale(self.zoom_ratio, self.zoom_ratio)
1833
-        cr.translate(-self.x, -self.y)
1834
-
1835
-        self.graph.draw(cr, highlight_items=self.highlight)
1836
-
1837
-    def get_drag_action(self, event):
1838
-        state = event.state
1839
-        if event.button in (1, 2): # left or middle button
1840
-            modifiers = Gtk.accelerator_get_default_mod_mask()
1841
-            if state & modifiers == Gdk.ModifierType.CONTROL_MASK:
1842
-                return ZoomAction
1843
-            elif state & modifiers == Gdk.ModifierType.SHIFT_MASK:
1844
-                return ZoomAreaAction
1845
-            else:
1846
-                return PanAction
1847
-        return NullAction
1848
-
1849
-    def on_area_button_press(self, area, event):
1850
-        self.animation.stop()
1851
-        self.drag_action.abort()
1852
-        action_type = self.get_drag_action(event)
1853
-        self.drag_action = action_type(self)
1854
-        self.drag_action.on_button_press(event)
1855
-        self.presstime = time.time()
1856
-        self.pressx = event.x
1857
-        self.pressy = event.y
1858
-        return False
1859
-
1860
-    def is_click(self, event, click_fuzz=4, click_timeout=1.0):
1861
-        assert event.type == Gdk.EventType.BUTTON_RELEASE
1862
-        if self.presstime is None:
1863
-            # got a button release without seeing the press?
1864
-            return False
1865
-        # XXX instead of doing this complicated logic, shouldn't we listen
1866
-        # for gtk's clicked event instead?
1867
-        deltax = self.pressx - event.x
1868
-        deltay = self.pressy - event.y
1869
-        return (time.time() < self.presstime + click_timeout
1870
-                and math.hypot(deltax, deltay) < click_fuzz)
1871
-
1872
-    def on_click(self, element, event):
1873
-        """Override this method in subclass to process
1874
-        click events. Note that element can be None
1875
-        (click on empty space)."""
1876
-        return False
1877
-
1878
-    def on_area_button_release(self, area, event):
1879
-        self.drag_action.on_button_release(event)
1880
-        self.drag_action = NullAction(self)
1881
-        x, y = int(event.x), int(event.y)
1882
-        if self.is_click(event):
1883
-            el = self.get_element(x, y)
1884
-            if self.on_click(el, event):
1885
-                return True
1886
-
1887
-            if event.button == 1:
1888
-                url = self.get_url(x, y)
1889
-                if url is not None:
1890
-                    self.emit('clicked', url.url, event)
1891
-                else:
1892
-                    jump = self.get_jump(x, y)
1893
-                    if jump is not None:
1894
-                        self.animate_to(jump.x, jump.y)
1895
-
1896
-                return True
1897
-
1898
-        if event.button == 1 or event.button == 2:
1899
-            return True
1900
-        return False
1901
-
1902
-    def on_area_scroll_event(self, area, event):
1903
-        if event.direction == Gdk.ScrollDirection.UP:
1904
-            self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT,
1905
-                            pos=(event.x, event.y))
1906
-            return True
1907
-        if event.direction == Gdk.ScrollDirection.DOWN:
1908
-            self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT,
1909
-                            pos=(event.x, event.y))
1910
-            return True
1911
-        return False
1912
-
1913
-    def on_area_motion_notify(self, area, event):
1914
-        self.drag_action.on_motion_notify(event)
1915
-        return True
1916
-
1917
-    def on_area_size_allocate(self, area, allocation):
1918
-        if self.zoom_to_fit_on_resize:
1919
-            self.zoom_to_fit()
1920
-
1921
-    def animate_to(self, x, y):
1922
-        self.animation = ZoomToAnimation(self, x, y)
1923
-        self.animation.start()
1924
-
1925
-    def window2graph(self, x, y):
1926
-        rect = self.get_allocation()
1927
-        x -= 0.5*rect.width
1928
-        y -= 0.5*rect.height
1929
-        x /= self.zoom_ratio
1930
-        y /= self.zoom_ratio
1931
-        x += self.x
1932
-        y += self.y
1933
-        return x, y
1934
-
1935
-    def get_element(self, x, y):
1936
-        x, y = self.window2graph(x, y)
1937
-        return self.graph.get_element(x, y)
1938
-
1939
-    def get_url(self, x, y):
1940
-        x, y = self.window2graph(x, y)
1941
-        return self.graph.get_url(x, y)
1942
-
1943
-    def get_jump(self, x, y):
1944
-        x, y = self.window2graph(x, y)
1945
-        return self.graph.get_jump(x, y)
1946
-
1947
-
1948
-class FindMenuToolAction(Gtk.Action):
1949
-    __gtype_name__ = "FindMenuToolAction"
1950
-
1951
-    def do_create_tool_item(self):
1952
-        return Gtk.ToolItem()
1953
-
1954
-
1955
-class DotWindow(Gtk.Window):
1956
-
1957
-    ui = '''
1958
-    <ui>
1959
-        <toolbar name="ToolBar">
1960
-            <toolitem action="Open"/>
1961
-            <toolitem action="Reload"/>
1962
-            <toolitem action="Print"/>
1963
-            <separator/>
1964
-            <toolitem action="ZoomIn"/>
1965
-            <toolitem action="ZoomOut"/>
1966
-            <toolitem action="ZoomFit"/>
1967
-            <toolitem action="Zoom100"/>
1968
-            <separator/>
1969
-            <toolitem name="Find" action="Find"/>
1970
-        </toolbar>
1971
-    </ui>
1972
-    '''
1973
-
1974
-    base_title = 'Dot Viewer'
1975
-
1976
-    def __init__(self, widget=None, width=512, height=512):
1977
-        Gtk.Window.__init__(self)
1978
-
1979
-        self.graph = Graph()
1980
-
1981
-        window = self
1982
-
1983
-        window.set_title(self.base_title)
1984
-        window.set_default_size(width, height)
1985
-        vbox = Gtk.VBox()
1986
-        window.add(vbox)
1987
-
1988
-        self.dotwidget = widget or DotWidget()
1989
-        self.dotwidget.connect("error", lambda e, m: self.error_dialog(m))
1990
-
1991
-        # Create a UIManager instance
1992
-        uimanager = self.uimanager = Gtk.UIManager()
1993
-
1994
-        # Add the accelerator group to the toplevel window
1995
-        accelgroup = uimanager.get_accel_group()
1996
-        window.add_accel_group(accelgroup)
1997
-
1998
-        # Create an ActionGroup
1999
-        actiongroup = Gtk.ActionGroup('Actions')
2000
-        self.actiongroup = actiongroup
2001
-
2002
-        # Create actions
2003
-        actiongroup.add_actions((
2004
-            ('Open', Gtk.STOCK_OPEN, None, None, None, self.on_open),
2005
-            ('Reload', Gtk.STOCK_REFRESH, None, None, None, self.on_reload),
2006
-            ('Print', Gtk.STOCK_PRINT, None, None, "Prints the currently visible part of the graph", self.dotwidget.on_print),
2007
-            ('ZoomIn', Gtk.STOCK_ZOOM_IN, None, None, None, self.dotwidget.on_zoom_in),
2008
-            ('ZoomOut', Gtk.STOCK_ZOOM_OUT, None, None, None, self.dotwidget.on_zoom_out),
2009
-            ('ZoomFit', Gtk.STOCK_ZOOM_FIT, None, None, None, self.dotwidget.on_zoom_fit),
2010
-            ('Zoom100', Gtk.STOCK_ZOOM_100, None, None, None, self.dotwidget.on_zoom_100),
2011
-        ))
2012
-
2013
-        find_action = FindMenuToolAction("Find", None,
2014
-                                          "Find a node by name", None)
2015
-        actiongroup.add_action(find_action)
2016
-
2017
-        # Add the actiongroup to the uimanager
2018
-        uimanager.insert_action_group(actiongroup, 0)
2019
-
2020
-        # Add a UI descrption
2021
-        uimanager.add_ui_from_string(self.ui)
2022
-
2023
-        # Create a Toolbar
2024
-        toolbar = uimanager.get_widget('/ToolBar')
2025
-        vbox.pack_start(toolbar, False, False, 0)
2026
-
2027
-        vbox.pack_start(self.dotwidget, True, True, 0)
2028
-
2029
-        self.last_open_dir = "."
2030
-
2031
-        self.set_focus(self.dotwidget)
2032
-
2033
-        # Add Find text search
2034
-        find_toolitem = uimanager.get_widget('/ToolBar/Find')
2035
-        self.textentry = Gtk.Entry(max_length=20)
2036
-        self.textentry.set_icon_from_stock(0, Gtk.STOCK_FIND)
2037
-        find_toolitem.add(self.textentry)
2038
-
2039
-        self.textentry.set_activates_default(True)
2040
-        self.textentry.connect ("activate", self.textentry_activate, self.textentry);
2041
-        self.textentry.connect ("changed", self.textentry_changed, self.textentry);
2042
-
2043
-        self.show_all()
2044
-
2045
-    def find_text(self, entry_text):
2046
-        found_items = []
2047
-        dot_widget = self.dotwidget
2048
-        regexp = re.compile(entry_text)
2049
-        for element in dot_widget.graph.nodes + dot_widget.graph.edges:
2050
-            if element.search_text(regexp):
2051
-                found_items.append(element)
2052
-        return found_items
2053
-
2054
-    def textentry_changed(self, widget, entry):
2055
-        entry_text = entry.get_text()
2056
-        dot_widget = self.dotwidget        
2057
-        if not entry_text:
2058
-            dot_widget.set_highlight(None, search=True)
2059
-            return
2060
-        
2061
-        found_items = self.find_text(entry_text)
2062
-        dot_widget.set_highlight(found_items, search=True)
2063
-
2064
-    def textentry_activate(self, widget, entry):
2065
-        entry_text = entry.get_text()
2066
-        dot_widget = self.dotwidget        
2067
-        if not entry_text:
2068
-            dot_widget.set_highlight(None, search=True)
2069
-            return;
2070
-        
2071
-        found_items = self.find_text(entry_text)
2072
-        dot_widget.set_highlight(found_items, search=True)
2073
-        if(len(found_items) == 1):
2074
-            dot_widget.animate_to(found_items[0].x, found_items[0].y)
2075
-
2076
-    def set_filter(self, filter):
2077
-        self.dotwidget.set_filter(filter)
2078
-
2079
-    def set_dotcode(self, dotcode, filename=None):
2080
-        if self.dotwidget.set_dotcode(dotcode, filename):
2081
-            self.update_title(filename)
2082
-            self.dotwidget.zoom_to_fit()
2083
-
2084
-    def set_xdotcode(self, xdotcode, filename=None):
2085
-        if self.dotwidget.set_xdotcode(xdotcode):
2086
-            self.update_title(filename)
2087
-            self.dotwidget.zoom_to_fit()
2088
-        
2089
-    def update_title(self, filename=None):
2090
-        if filename is None:
2091
-            self.set_title(self.base_title)
2092
-        else:
2093
-            self.set_title(os.path.basename(filename) + ' - ' + self.base_title)
2094
-
2095
-    def open_file(self, filename):
2096
-        try:
2097
-            fp = open(filename, 'rt')
2098
-            self.set_dotcode(fp.read(), filename)
2099
-            fp.close()
2100
-        except IOError as ex:
2101
-            self.error_dialog(str(ex))
2102
-
2103
-    def on_open(self, action):
2104
-        chooser = Gtk.FileChooserDialog(parent=self,
2105
-                                        title="Open dot File",
2106
-                                        action=Gtk.FileChooserAction.OPEN,
2107
-                                        buttons=(Gtk.STOCK_CANCEL,
2108
-                                                 Gtk.ResponseType.CANCEL,
2109
-                                                 Gtk.STOCK_OPEN,
2110
-                                                 Gtk.ResponseType.OK))
2111
-        chooser.set_default_response(Gtk.ResponseType.OK)
2112
-        chooser.set_current_folder(self.last_open_dir)
2113
-        filter = Gtk.FileFilter()
2114
-        filter.set_name("Graphviz dot files")
2115
-        filter.add_pattern("*.dot")
2116
-        chooser.add_filter(filter)
2117
-        filter = Gtk.FileFilter()
2118
-        filter.set_name("All files")
2119
-        filter.add_pattern("*")
2120
-        chooser.add_filter(filter)
2121
-        if chooser.run() == Gtk.ResponseType.OK:
2122
-            filename = chooser.get_filename()
2123
-            self.last_open_dir = chooser.get_current_folder()
2124
-            chooser.destroy()
2125
-            self.open_file(filename)
2126
-        else:
2127
-            chooser.destroy()
2128
-
2129
-    def on_reload(self, action):
2130
-        self.dotwidget.reload()
2131
-
2132
-    def error_dialog(self, message):
2133
-        dlg = Gtk.MessageDialog(parent=self,
2134
-                                type=Gtk.MessageType.ERROR,
2135
-                                message_format=message,
2136
-                                buttons=Gtk.ButtonsType.OK)
2137
-        dlg.set_title(self.base_title)
2138
-        dlg.run()
2139
-        dlg.destroy()
2140
-
2141
-
2142
-class OptionParser(optparse.OptionParser):
2143
-
2144
-    def format_epilog(self, formatter):
2145
-        # Prevent stripping the newlines in epilog message
2146
-        # http://stackoverflow.com/questions/1857346/python-optparse-how-to-include-additional-info-in-usage-output
2147
-        return self.epilog
2148
-
2149
-
2150
-def main():
2151
-
2152
-    parser = OptionParser(
2153
-        usage='\n\t%prog [file]',
2154
-        epilog='''
2155
-Shortcuts:
2156
-  Up, Down, Left, Right     scroll
2157
-  PageUp, +, =              zoom in
2158
-  PageDown, -               zoom out
2159
-  R                         reload dot file
2160
-  F                         find
2161
-  Q                         quit
2162
-  P                         print
2163
-  Escape                    halt animation
2164
-  Ctrl-drag                 zoom in/out
2165
-  Shift-drag                zooms an area
2166
-'''
2167
-    )
2168
-    parser.add_option(
2169
-        '-f', '--filter',
2170
-        type='choice', choices=('dot', 'neato', 'twopi', 'circo', 'fdp'),
2171
-        dest='filter', default='dot',
2172
-        help='graphviz filter: dot, neato, twopi, circo, or fdp [default: %default]')
2173
-    parser.add_option(
2174
-        '-n', '--no-filter',
2175
-        action='store_const', const=None, dest='filter',
2176
-        help='assume input is already filtered into xdot format (use e.g. dot -Txdot)')
2177
-    parser.add_option(
2178
-        '-g', None,
2179
-        action='store', dest='geometry',
2180
-        help='default window size in form WxH')
2181
-
2182
-    (options, args) = parser.parse_args(sys.argv[1:])
2183
-    if len(args) > 1:
2184
-        parser.error('incorrect number of arguments')
2185
-
2186
-    width = height = 512
2187
-    if options.geometry:
2188
-        try:
2189
-            width,height = (int(i) for i in options.geometry.split('x'))
2190
-        except ValueError:
2191
-            parser.error('invalid window geometry')
2192
-
2193
-    win = DotWindow(width=width, height=height)
2194
-    win.connect('delete-event', Gtk.main_quit)
2195
-    win.set_filter(options.filter)
2196
-    if len(args) >= 1:
2197
-        if args[0] == '-':
2198
-            win.set_dotcode(sys.stdin.read())
2199
-        else:
2200
-            win.open_file(args[0])
2201
-    Gtk.main()
2202
-
2203
-
2204
-# Apache-Style Software License for ColorBrewer software and ColorBrewer Color
2205
-# Schemes, Version 1.1
2206
-# 
2207
-# Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State
2208
-# University. All rights reserved.
2209
-# 
2210
-# Redistribution and use in source and binary forms, with or without
2211
-# modification, are permitted provided that the following conditions are met:
2212
-# 
2213
-#    1. Redistributions as source code must retain the above copyright notice,
2214
-#    this list of conditions and the following disclaimer.  
2215
-#
2216
-#    2. The end-user documentation included with the redistribution, if any,
2217
-#    must include the following acknowledgment:
2218
-# 
2219
-#       This product includes color specifications and designs developed by
2220
-#       Cynthia Brewer (http://colorbrewer.org/).
2221
-# 
2222
-#    Alternately, this acknowledgment may appear in the software itself, if and
2223
-#    wherever such third-party acknowledgments normally appear.  
2224
-#
2225
-#    3. The name "ColorBrewer" must not be used to endorse or promote products
2226
-#    derived from this software without prior written permission. For written
2227
-#    permission, please contact Cynthia Brewer at cbrewer@psu.edu.
2228
-#
2229
-#    4. Products derived from this software may not be called "ColorBrewer",
2230
-#    nor may "ColorBrewer" appear in their name, without prior written
2231
-#    permission of Cynthia Brewer. 
2232
-# 
2233
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES,
2234
-# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
2235
-# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CYNTHIA
2236
-# BREWER, MARK HARROWER, OR THE PENNSYLVANIA STATE UNIVERSITY BE LIABLE FOR ANY
2237
-# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
2238
-# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
2239
-# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
2240
-# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
2241
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
2242
-# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
2243
-brewer_colors = {
2244
-    'accent3': [(127, 201, 127), (190, 174, 212), (253, 192, 134)],
2245
-    'accent4': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153)],
2246
-    'accent5': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176)],
2247
-    'accent6': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176), (240, 2, 127)],
2248
-    'accent7': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176), (240, 2, 127), (191, 91, 23)],
2249
-    'accent8': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176), (240, 2, 127), (191, 91, 23), (102, 102, 102)],
2250
-    'blues3': [(222, 235, 247), (158, 202, 225), (49, 130, 189)],
2251
-    'blues4': [(239, 243, 255), (189, 215, 231), (107, 174, 214), (33, 113, 181)],
2252
-    'blues5': [(239, 243, 255), (189, 215, 231), (107, 174, 214), (49, 130, 189), (8, 81, 156)],
2253
-    'blues6': [(239, 243, 255), (198, 219, 239), (158, 202, 225), (107, 174, 214), (49, 130, 189), (8, 81, 156)],
2254
-    'blues7': [(239, 243, 255), (198, 219, 239), (158, 202, 225), (107, 174, 214), (66, 146, 198), (33, 113, 181), (8, 69, 148)],
2255
-    'blues8': [(247, 251, 255), (222, 235, 247), (198, 219, 239), (158, 202, 225), (107, 174, 214), (66, 146, 198), (33, 113, 181), (8, 69, 148)],
2256
-    'blues9': [(247, 251, 255), (222, 235, 247), (198, 219, 239), (158, 202, 225), (107, 174, 214), (66, 146, 198), (33, 113, 181), (8, 81, 156), (8, 48, 107)],
2257
-    'brbg10': [(84, 48, 5), (0, 60, 48), (140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (199, 234, 229), (128, 205, 193), (53, 151, 143), (1, 102, 94)],
2258
-    'brbg11': [(84, 48, 5), (1, 102, 94), (0, 60, 48), (140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (245, 245, 245), (199, 234, 229), (128, 205, 193), (53, 151, 143)],
2259
-    'brbg3': [(216, 179, 101), (245, 245, 245), (90, 180, 172)],
2260
-    'brbg4': [(166, 97, 26), (223, 194, 125), (128, 205, 193), (1, 133, 113)],
2261
-    'brbg5': [(166, 97, 26), (223, 194, 125), (245, 245, 245), (128, 205, 193), (1, 133, 113)],
2262
-    'brbg6': [(140, 81, 10), (216, 179, 101), (246, 232, 195), (199, 234, 229), (90, 180, 172), (1, 102, 94)],
2263
-    'brbg7': [(140, 81, 10), (216, 179, 101), (246, 232, 195), (245, 245, 245), (199, 234, 229), (90, 180, 172), (1, 102, 94)],
2264
-    'brbg8': [(140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (199, 234, 229), (128, 205, 193), (53, 151, 143), (1, 102, 94)],
2265
-    'brbg9': [(140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (245, 245, 245), (199, 234, 229), (128, 205, 193), (53, 151, 143), (1, 102, 94)],
2266
-    'bugn3': [(229, 245, 249), (153, 216, 201), (44, 162, 95)],
2267
-    'bugn4': [(237, 248, 251), (178, 226, 226), (102, 194, 164), (35, 139, 69)],
2268
-    'bugn5': [(237, 248, 251), (178, 226, 226), (102, 194, 164), (44, 162, 95), (0, 109, 44)],
2269
-    'bugn6': [(237, 248, 251), (204, 236, 230), (153, 216, 201), (102, 194, 164), (44, 162, 95), (0, 109, 44)],
2270
-    'bugn7': [(237, 248, 251), (204, 236, 230), (153, 216, 201), (102, 194, 164), (65, 174, 118), (35, 139, 69), (0, 88, 36)],
2271
-    'bugn8': [(247, 252, 253), (229, 245, 249), (204, 236, 230), (153, 216, 201), (102, 194, 164), (65, 174, 118), (35, 139, 69), (0, 88, 36)],
2272
-    'bugn9': [(247, 252, 253), (229, 245, 249), (204, 236, 230), (153, 216, 201), (102, 194, 164), (65, 174, 118), (35, 139, 69), (0, 109, 44), (0, 68, 27)],
2273
-    'bupu3': [(224, 236, 244), (158, 188, 218), (136, 86, 167)],
2274
-    'bupu4': [(237, 248, 251), (179, 205, 227), (140, 150, 198), (136, 65, 157)],
2275
-    'bupu5': [(237, 248, 251), (179, 205, 227), (140, 150, 198), (136, 86, 167), (129, 15, 124)],
2276
-    'bupu6': [(237, 248, 251), (191, 211, 230), (158, 188, 218), (140, 150, 198), (136, 86, 167), (129, 15, 124)],
2277
-    'bupu7': [(237, 248, 251), (191, 211, 230), (158, 188, 218), (140, 150, 198), (140, 107, 177), (136, 65, 157), (110, 1, 107)],
2278
-    'bupu8': [(247, 252, 253), (224, 236, 244), (191, 211, 230), (158, 188, 218), (140, 150, 198), (140, 107, 177), (136, 65, 157), (110, 1, 107)],
2279
-    'bupu9': [(247, 252, 253), (224, 236, 244), (191, 211, 230), (158, 188, 218), (140, 150, 198), (140, 107, 177), (136, 65, 157), (129, 15, 124), (77, 0, 75)],
2280
-    'dark23': [(27, 158, 119), (217, 95, 2), (117, 112, 179)],
2281
-    'dark24': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138)],
2282
-    'dark25': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30)],
2283
-    'dark26': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30), (230, 171, 2)],
2284
-    'dark27': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30), (230, 171, 2), (166, 118, 29)],
2285
-    'dark28': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30), (230, 171, 2), (166, 118, 29), (102, 102, 102)],
2286
-    'gnbu3': [(224, 243, 219), (168, 221, 181), (67, 162, 202)],
2287
-    'gnbu4': [(240, 249, 232), (186, 228, 188), (123, 204, 196), (43, 140, 190)],
2288
-    'gnbu5': [(240, 249, 232), (186, 228, 188), (123, 204, 196), (67, 162, 202), (8, 104, 172)],
2289
-    'gnbu6': [(240, 249, 232), (204, 235, 197), (168, 221, 181), (123, 204, 196), (67, 162, 202), (8, 104, 172)],
2290
-    'gnbu7': [(240, 249, 232), (204, 235, 197), (168, 221, 181), (123, 204, 196), (78, 179, 211), (43, 140, 190), (8, 88, 158)],
2291
-    'gnbu8': [(247, 252, 240), (224, 243, 219), (204, 235, 197), (168, 221, 181), (123, 204, 196), (78, 179, 211), (43, 140, 190), (8, 88, 158)],
2292
-    'gnbu9': [(247, 252, 240), (224, 243, 219), (204, 235, 197), (168, 221, 181), (123, 204, 196), (78, 179, 211), (43, 140, 190), (8, 104, 172), (8, 64, 129)],
2293
-    'greens3': [(229, 245, 224), (161, 217, 155), (49, 163, 84)],
2294
-    'greens4': [(237, 248, 233), (186, 228, 179), (116, 196, 118), (35, 139, 69)],
2295
-    'greens5': [(237, 248, 233), (186, 228, 179), (116, 196, 118), (49, 163, 84), (0, 109, 44)],
2296
-    'greens6': [(237, 248, 233), (199, 233, 192), (161, 217, 155), (116, 196, 118), (49, 163, 84), (0, 109, 44)],
2297
-    'greens7': [(237, 248, 233), (199, 233, 192), (161, 217, 155), (116, 196, 118), (65, 171, 93), (35, 139, 69), (0, 90, 50)],
2298
-    'greens8': [(247, 252, 245), (229, 245, 224), (199, 233, 192), (161, 217, 155), (116, 196, 118), (65, 171, 93), (35, 139, 69), (0, 90, 50)],
2299
-    'greens9': [(247, 252, 245), (229, 245, 224), (199, 233, 192), (161, 217, 155), (116, 196, 118), (65, 171, 93), (35, 139, 69), (0, 109, 44), (0, 68, 27)],
2300
-    'greys3': [(240, 240, 240), (189, 189, 189), (99, 99, 99)],
2301
-    'greys4': [(247, 247, 247), (204, 204, 204), (150, 150, 150), (82, 82, 82)],
2302
-    'greys5': [(247, 247, 247), (204, 204, 204), (150, 150, 150), (99, 99, 99), (37, 37, 37)],
2303
-    'greys6': [(247, 247, 247), (217, 217, 217), (189, 189, 189), (150, 150, 150), (99, 99, 99), (37, 37, 37)],
2304
-    'greys7': [(247, 247, 247), (217, 217, 217), (189, 189, 189), (150, 150, 150), (115, 115, 115), (82, 82, 82), (37, 37, 37)],
2305
-    'greys8': [(255, 255, 255), (240, 240, 240), (217, 217, 217), (189, 189, 189), (150, 150, 150), (115, 115, 115), (82, 82, 82), (37, 37, 37)],
2306
-    'greys9': [(255, 255, 255), (240, 240, 240), (217, 217, 217), (189, 189, 189), (150, 150, 150), (115, 115, 115), (82, 82, 82), (37, 37, 37), (0, 0, 0)],
2307
-    'oranges3': [(254, 230, 206), (253, 174, 107), (230, 85, 13)],
2308
-    'oranges4': [(254, 237, 222), (253, 190, 133), (253, 141, 60), (217, 71, 1)],
2309
-    'oranges5': [(254, 237, 222), (253, 190, 133), (253, 141, 60), (230, 85, 13), (166, 54, 3)],
2310
-    'oranges6': [(254, 237, 222), (253, 208, 162), (253, 174, 107), (253, 141, 60), (230, 85, 13), (166, 54, 3)],
2311
-    'oranges7': [(254, 237, 222), (253, 208, 162), (253, 174, 107), (253, 141, 60), (241, 105, 19), (217, 72, 1), (140, 45, 4)],
2312
-    'oranges8': [(255, 245, 235), (254, 230, 206), (253, 208, 162), (253, 174, 107), (253, 141, 60), (241, 105, 19), (217, 72, 1), (140, 45, 4)],
2313
-    'oranges9': [(255, 245, 235), (254, 230, 206), (253, 208, 162), (253, 174, 107), (253, 141, 60), (241, 105, 19), (217, 72, 1), (166, 54, 3), (127, 39, 4)],
2314
-    'orrd3': [(254, 232, 200), (253, 187, 132), (227, 74, 51)],
2315
-    'orrd4': [(254, 240, 217), (253, 204, 138), (252, 141, 89), (215, 48, 31)],
2316
-    'orrd5': [(254, 240, 217), (253, 204, 138), (252, 141, 89), (227, 74, 51), (179, 0, 0)],
2317
-    'orrd6': [(254, 240, 217), (253, 212, 158), (253, 187, 132), (252, 141, 89), (227, 74, 51), (179, 0, 0)],
2318
-    'orrd7': [(254, 240, 217), (253, 212, 158), (253, 187, 132), (252, 141, 89), (239, 101, 72), (215, 48, 31), (153, 0, 0)],
2319
-    'orrd8': [(255, 247, 236), (254, 232, 200), (253, 212, 158), (253, 187, 132), (252, 141, 89), (239, 101, 72), (215, 48, 31), (153, 0, 0)],
2320
-    'orrd9': [(255, 247, 236), (254, 232, 200), (253, 212, 158), (253, 187, 132), (252, 141, 89), (239, 101, 72), (215, 48, 31), (179, 0, 0), (127, 0, 0)],
2321
-    'paired10': [(166, 206, 227), (106, 61, 154), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
2322
-    'paired11': [(166, 206, 227), (106, 61, 154), (255, 255, 153), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
2323
-    'paired12': [(166, 206, 227), (106, 61, 154), (255, 255, 153), (177, 89, 40), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
2324
-    'paired3': [(166, 206, 227), (31, 120, 180), (178, 223, 138)],
2325
-    'paired4': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44)],
2326
-    'paired5': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153)],
2327
-    'paired6': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28)],
2328
-    'paired7': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111)],
2329
-    'paired8': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0)],
2330
-    'paired9': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
2331
-    'pastel13': [(251, 180, 174), (179, 205, 227), (204, 235, 197)],
2332
-    'pastel14': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228)],
2333
-    'pastel15': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166)],
2334
-    'pastel16': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204)],
2335
-    'pastel17': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204), (229, 216, 189)],
2336
-    'pastel18': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204), (229, 216, 189), (253, 218, 236)],
2337
-    'pastel19': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204), (229, 216, 189), (253, 218, 236), (242, 242, 242)],
2338
-    'pastel23': [(179, 226, 205), (253, 205, 172), (203, 213, 232)],
2339
-    'pastel24': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228)],
2340
-    'pastel25': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201)],
2341
-    'pastel26': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201), (255, 242, 174)],
2342
-    'pastel27': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201), (255, 242, 174), (241, 226, 204)],
2343
-    'pastel28': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201), (255, 242, 174), (241, 226, 204), (204, 204, 204)],
2344
-    'piyg10': [(142, 1, 82), (39, 100, 25), (197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (230, 245, 208), (184, 225, 134), (127, 188, 65), (77, 146, 33)],
2345
-    'piyg11': [(142, 1, 82), (77, 146, 33), (39, 100, 25), (197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (247, 247, 247), (230, 245, 208), (184, 225, 134), (127, 188, 65)],
2346
-    'piyg3': [(233, 163, 201), (247, 247, 247), (161, 215, 106)],
2347
-    'piyg4': [(208, 28, 139), (241, 182, 218), (184, 225, 134), (77, 172, 38)],
2348
-    'piyg5': [(208, 28, 139), (241, 182, 218), (247, 247, 247), (184, 225, 134), (77, 172, 38)],
2349
-    'piyg6': [(197, 27, 125), (233, 163, 201), (253, 224, 239), (230, 245, 208), (161, 215, 106), (77, 146, 33)],
2350
-    'piyg7': [(197, 27, 125), (233, 163, 201), (253, 224, 239), (247, 247, 247), (230, 245, 208), (161, 215, 106), (77, 146, 33)],
2351
-    'piyg8': [(197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (230, 245, 208), (184, 225, 134), (127, 188, 65), (77, 146, 33)],
2352
-    'piyg9': [(197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (247, 247, 247), (230, 245, 208), (184, 225, 134), (127, 188, 65), (77, 146, 33)],
2353
-    'prgn10': [(64, 0, 75), (0, 68, 27), (118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (217, 240, 211), (166, 219, 160), (90, 174, 97), (27, 120, 55)],
2354
-    'prgn11': [(64, 0, 75), (27, 120, 55), (0, 68, 27), (118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (247, 247, 247), (217, 240, 211), (166, 219, 160), (90, 174, 97)],
2355
-    'prgn3': [(175, 141, 195), (247, 247, 247), (127, 191, 123)],
2356
-    'prgn4': [(123, 50, 148), (194, 165, 207), (166, 219, 160), (0, 136, 55)],
2357
-    'prgn5': [(123, 50, 148), (194, 165, 207), (247, 247, 247), (166, 219, 160), (0, 136, 55)],
2358
-    'prgn6': [(118, 42, 131), (175, 141, 195), (231, 212, 232), (217, 240, 211), (127, 191, 123), (27, 120, 55)],
2359
-    'prgn7': [(118, 42, 131), (175, 141, 195), (231, 212, 232), (247, 247, 247), (217, 240, 211), (127, 191, 123), (27, 120, 55)],
2360
-    'prgn8': [(118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (217, 240, 211), (166, 219, 160), (90, 174, 97), (27, 120, 55)],
2361
-    'prgn9': [(118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (247, 247, 247), (217, 240, 211), (166, 219, 160), (90, 174, 97), (27, 120, 55)],
2362
-    'pubu3': [(236, 231, 242), (166, 189, 219), (43, 140, 190)],
2363
-    'pubu4': [(241, 238, 246), (189, 201, 225), (116, 169, 207), (5, 112, 176)],
2364
-    'pubu5': [(241, 238, 246), (189, 201, 225), (116, 169, 207), (43, 140, 190), (4, 90, 141)],
2365
-    'pubu6': [(241, 238, 246), (208, 209, 230), (166, 189, 219), (116, 169, 207), (43, 140, 190), (4, 90, 141)],
2366
-    'pubu7': [(241, 238, 246), (208, 209, 230), (166, 189, 219), (116, 169, 207), (54, 144, 192), (5, 112, 176), (3, 78, 123)],
2367
-    'pubu8': [(255, 247, 251), (236, 231, 242), (208, 209, 230), (166, 189, 219), (116, 169, 207), (54, 144, 192), (5, 112, 176), (3, 78, 123)],
2368
-    'pubu9': [(255, 247, 251), (236, 231, 242), (208, 209, 230), (166, 189, 219), (116, 169, 207), (54, 144, 192), (5, 112, 176), (4, 90, 141), (2, 56, 88)],
2369
-    'pubugn3': [(236, 226, 240), (166, 189, 219), (28, 144, 153)],
2370
-    'pubugn4': [(246, 239, 247), (189, 201, 225), (103, 169, 207), (2, 129, 138)],
2371
-    'pubugn5': [(246, 239, 247), (189, 201, 225), (103, 169, 207), (28, 144, 153), (1, 108, 89)],
2372
-    'pubugn6': [(246, 239, 247), (208, 209, 230), (166, 189, 219), (103, 169, 207), (28, 144, 153), (1, 108, 89)],
2373
-    'pubugn7': [(246, 239, 247), (208, 209, 230), (166, 189, 219), (103, 169, 207), (54, 144, 192), (2, 129, 138), (1, 100, 80)],
2374
-    'pubugn8': [(255, 247, 251), (236, 226, 240), (208, 209, 230), (166, 189, 219), (103, 169, 207), (54, 144, 192), (2, 129, 138), (1, 100, 80)],
2375
-    'pubugn9': [(255, 247, 251), (236, 226, 240), (208, 209, 230), (166, 189, 219), (103, 169, 207), (54, 144, 192), (2, 129, 138), (1, 108, 89), (1, 70, 54)],
2376
-    'puor10': [(127, 59, 8), (45, 0, 75), (179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (216, 218, 235), (178, 171, 210), (128, 115, 172), (84, 39, 136)],
2377
-    'puor11': [(127, 59, 8), (84, 39, 136), (45, 0, 75), (179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (247, 247, 247), (216, 218, 235), (178, 171, 210), (128, 115, 172)],
2378
-    'puor3': [(241, 163, 64), (247, 247, 247), (153, 142, 195)],
2379
-    'puor4': [(230, 97, 1), (253, 184, 99), (178, 171, 210), (94, 60, 153)],
2380
-    'puor5': [(230, 97, 1), (253, 184, 99), (247, 247, 247), (178, 171, 210), (94, 60, 153)],
2381
-    'puor6': [(179, 88, 6), (241, 163, 64), (254, 224, 182), (216, 218, 235), (153, 142, 195), (84, 39, 136)],
2382
-    'puor7': [(179, 88, 6), (241, 163, 64), (254, 224, 182), (247, 247, 247), (216, 218, 235), (153, 142, 195), (84, 39, 136)],
2383
-    'puor8': [(179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (216, 218, 235), (178, 171, 210), (128, 115, 172), (84, 39, 136)],
2384
-    'puor9': [(179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (247, 247, 247), (216, 218, 235), (178, 171, 210), (128, 115, 172), (84, 39, 136)],
2385
-    'purd3': [(231, 225, 239), (201, 148, 199), (221, 28, 119)],
2386
-    'purd4': [(241, 238, 246), (215, 181, 216), (223, 101, 176), (206, 18, 86)],
2387
-    'purd5': [(241, 238, 246), (215, 181, 216), (223, 101, 176), (221, 28, 119), (152, 0, 67)],
2388
-    'purd6': [(241, 238, 246), (212, 185, 218), (201, 148, 199), (223, 101, 176), (221, 28, 119), (152, 0, 67)],
2389
-    'purd7': [(241, 238, 246), (212, 185, 218), (201, 148, 199), (223, 101, 176), (231, 41, 138), (206, 18, 86), (145, 0, 63)],
2390
-    'purd8': [(247, 244, 249), (231, 225, 239), (212, 185, 218), (201, 148, 199), (223, 101, 176), (231, 41, 138), (206, 18, 86), (145, 0, 63)],
2391
-    'purd9': [(247, 244, 249), (231, 225, 239), (212, 185, 218), (201, 148, 199), (223, 101, 176), (231, 41, 138), (206, 18, 86), (152, 0, 67), (103, 0, 31)],
2392
-    'purples3': [(239, 237, 245), (188, 189, 220), (117, 107, 177)],
2393
-    'purples4': [(242, 240, 247), (203, 201, 226), (158, 154, 200), (106, 81, 163)],
2394
-    'purples5': [(242, 240, 247), (203, 201, 226), (158, 154, 200), (117, 107, 177), (84, 39, 143)],
2395
-    'purples6': [(242, 240, 247), (218, 218, 235), (188, 189, 220), (158, 154, 200), (117, 107, 177), (84, 39, 143)],
2396
-    'purples7': [(242, 240, 247), (218, 218, 235), (188, 189, 220), (158, 154, 200), (128, 125, 186), (106, 81, 163), (74, 20, 134)],
2397
-    'purples8': [(252, 251, 253), (239, 237, 245), (218, 218, 235), (188, 189, 220), (158, 154, 200), (128, 125, 186), (106, 81, 163), (74, 20, 134)],
2398
-    'purples9': [(252, 251, 253), (239, 237, 245), (218, 218, 235), (188, 189, 220), (158, 154, 200), (128, 125, 186), (106, 81, 163), (84, 39, 143), (63, 0, 125)],
2399
-    'rdbu10': [(103, 0, 31), (5, 48, 97), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (209, 229, 240), (146, 197, 222), (67, 147, 195), (33, 102, 172)],
2400
-    'rdbu11': [(103, 0, 31), (33, 102, 172), (5, 48, 97), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (247, 247, 247), (209, 229, 240), (146, 197, 222), (67, 147, 195)],
2401
-    'rdbu3': [(239, 138, 98), (247, 247, 247), (103, 169, 207)],
2402
-    'rdbu4': [(202, 0, 32), (244, 165, 130), (146, 197, 222), (5, 113, 176)],
2403
-    'rdbu5': [(202, 0, 32), (244, 165, 130), (247, 247, 247), (146, 197, 222), (5, 113, 176)],
2404
-    'rdbu6': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (209, 229, 240), (103, 169, 207), (33, 102, 172)],
2405
-    'rdbu7': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (247, 247, 247), (209, 229, 240), (103, 169, 207), (33, 102, 172)],
2406
-    'rdbu8': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (209, 229, 240), (146, 197, 222), (67, 147, 195), (33, 102, 172)],
2407
-    'rdbu9': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (247, 247, 247), (209, 229, 240), (146, 197, 222), (67, 147, 195), (33, 102, 172)],
2408
-    'rdgy10': [(103, 0, 31), (26, 26, 26), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (224, 224, 224), (186, 186, 186), (135, 135, 135), (77, 77, 77)],
2409
-    'rdgy11': [(103, 0, 31), (77, 77, 77), (26, 26, 26), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (255, 255, 255), (224, 224, 224), (186, 186, 186), (135, 135, 135)],
2410
-    'rdgy3': [(239, 138, 98), (255, 255, 255), (153, 153, 153)],
2411
-    'rdgy4': [(202, 0, 32), (244, 165, 130), (186, 186, 186), (64, 64, 64)],
2412
-    'rdgy5': [(202, 0, 32), (244, 165, 130), (255, 255, 255), (186, 186, 186), (64, 64, 64)],
2413
-    'rdgy6': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (224, 224, 224), (153, 153, 153), (77, 77, 77)],
2414
-    'rdgy7': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (255, 255, 255), (224, 224, 224), (153, 153, 153), (77, 77, 77)],
2415
-    'rdgy8': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (224, 224, 224), (186, 186, 186), (135, 135, 135), (77, 77, 77)],
2416
-    'rdgy9': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (255, 255, 255), (224, 224, 224), (186, 186, 186), (135, 135, 135), (77, 77, 77)],
2417
-    'rdpu3': [(253, 224, 221), (250, 159, 181), (197, 27, 138)],
2418
-    'rdpu4': [(254, 235, 226), (251, 180, 185), (247, 104, 161), (174, 1, 126)],
2419
-    'rdpu5': [(254, 235, 226), (251, 180, 185), (247, 104, 161), (197, 27, 138), (122, 1, 119)],
2420
-    'rdpu6': [(254, 235, 226), (252, 197, 192), (250, 159, 181), (247, 104, 161), (197, 27, 138), (122, 1, 119)],
2421
-    'rdpu7': [(254, 235, 226), (252, 197, 192), (250, 159, 181), (247, 104, 161), (221, 52, 151), (174, 1, 126), (122, 1, 119)],
2422
-    'rdpu8': [(255, 247, 243), (253, 224, 221), (252, 197, 192), (250, 159, 181), (247, 104, 161), (221, 52, 151), (174, 1, 126), (122, 1, 119)],
2423
-    'rdpu9': [(255, 247, 243), (253, 224, 221), (252, 197, 192), (250, 159, 181), (247, 104, 161), (221, 52, 151), (174, 1, 126), (122, 1, 119), (73, 0, 106)],
2424
-    'rdylbu10': [(165, 0, 38), (49, 54, 149), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (224, 243, 248), (171, 217, 233), (116, 173, 209), (69, 117, 180)],
2425
-    'rdylbu11': [(165, 0, 38), (69, 117, 180), (49, 54, 149), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (255, 255, 191), (224, 243, 248), (171, 217, 233), (116, 173, 209)],
2426
-    'rdylbu3': [(252, 141, 89), (255, 255, 191), (145, 191, 219)],
2427
-    'rdylbu4': [(215, 25, 28), (253, 174, 97), (171, 217, 233), (44, 123, 182)],
2428
-    'rdylbu5': [(215, 25, 28), (253, 174, 97), (255, 255, 191), (171, 217, 233), (44, 123, 182)],
2429
-    'rdylbu6': [(215, 48, 39), (252, 141, 89), (254, 224, 144), (224, 243, 248), (145, 191, 219), (69, 117, 180)],
2430
-    'rdylbu7': [(215, 48, 39), (252, 141, 89), (254, 224, 144), (255, 255, 191), (224, 243, 248), (145, 191, 219), (69, 117, 180)],
2431
-    'rdylbu8': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (224, 243, 248), (171, 217, 233), (116, 173, 209), (69, 117, 180)],
2432
-    'rdylbu9': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (255, 255, 191), (224, 243, 248), (171, 217, 233), (116, 173, 209), (69, 117, 180)],
2433
-    'rdylgn10': [(165, 0, 38), (0, 104, 55), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (217, 239, 139), (166, 217, 106), (102, 189, 99), (26, 152, 80)],
2434
-    'rdylgn11': [(165, 0, 38), (26, 152, 80), (0, 104, 55), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (217, 239, 139), (166, 217, 106), (102, 189, 99)],
2435
-    'rdylgn3': [(252, 141, 89), (255, 255, 191), (145, 207, 96)],
2436
-    'rdylgn4': [(215, 25, 28), (253, 174, 97), (166, 217, 106), (26, 150, 65)],
2437
-    'rdylgn5': [(215, 25, 28), (253, 174, 97), (255, 255, 191), (166, 217, 106), (26, 150, 65)],
2438
-    'rdylgn6': [(215, 48, 39), (252, 141, 89), (254, 224, 139), (217, 239, 139), (145, 207, 96), (26, 152, 80)],
2439
-    'rdylgn7': [(215, 48, 39), (252, 141, 89), (254, 224, 139), (255, 255, 191), (217, 239, 139), (145, 207, 96), (26, 152, 80)],
2440
-    'rdylgn8': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (217, 239, 139), (166, 217, 106), (102, 189, 99), (26, 152, 80)],
2441
-    'rdylgn9': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (217, 239, 139), (166, 217, 106), (102, 189, 99), (26, 152, 80)],
2442
-    'reds3': [(254, 224, 210), (252, 146, 114), (222, 45, 38)],
2443
-    'reds4': [(254, 229, 217), (252, 174, 145), (251, 106, 74), (203, 24, 29)],
2444
-    'reds5': [(254, 229, 217), (252, 174, 145), (251, 106, 74), (222, 45, 38), (165, 15, 21)],
2445
-    'reds6': [(254, 229, 217), (252, 187, 161), (252, 146, 114), (251, 106, 74), (222, 45, 38), (165, 15, 21)],
2446
-    'reds7': [(254, 229, 217), (252, 187, 161), (252, 146, 114), (251, 106, 74), (239, 59, 44), (203, 24, 29), (153, 0, 13)],
2447
-    'reds8': [(255, 245, 240), (254, 224, 210), (252, 187, 161), (252, 146, 114), (251, 106, 74), (239, 59, 44), (203, 24, 29), (153, 0, 13)],
2448
-    'reds9': [(255, 245, 240), (254, 224, 210), (252, 187, 161), (252, 146, 114), (251, 106, 74), (239, 59, 44), (203, 24, 29), (165, 15, 21), (103, 0, 13)],
2449
-    'set13': [(228, 26, 28), (55, 126, 184), (77, 175, 74)],
2450
-    'set14': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163)],
2451
-    'set15': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0)],
2452
-    'set16': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51)],
2453
-    'set17': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51), (166, 86, 40)],
2454
-    'set18': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51), (166, 86, 40), (247, 129, 191)],
2455
-    'set19': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51), (166, 86, 40), (247, 129, 191), (153, 153, 153)],
2456
-    'set23': [(102, 194, 165), (252, 141, 98), (141, 160, 203)],
2457
-    'set24': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195)],
2458
-    'set25': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84)],
2459
-    'set26': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84), (255, 217, 47)],
2460
-    'set27': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84), (255, 217, 47), (229, 196, 148)],
2461
-    'set28': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84), (255, 217, 47), (229, 196, 148), (179, 179, 179)],
2462
-    'set310': [(141, 211, 199), (188, 128, 189), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
2463
-    'set311': [(141, 211, 199), (188, 128, 189), (204, 235, 197), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
2464
-    'set312': [(141, 211, 199), (188, 128, 189), (204, 235, 197), (255, 237, 111), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
2465
-    'set33': [(141, 211, 199), (255, 255, 179), (190, 186, 218)],
2466
-    'set34': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114)],
2467
-    'set35': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211)],
2468
-    'set36': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98)],
2469
-    'set37': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105)],
2470
-    'set38': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229)],
2471
-    'set39': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
2472
-    'spectral10': [(158, 1, 66), (94, 79, 162), (213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (230, 245, 152), (171, 221, 164), (102, 194, 165), (50, 136, 189)],
2473
-    'spectral11': [(158, 1, 66), (50, 136, 189), (94, 79, 162), (213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (230, 245, 152), (171, 221, 164), (102, 194, 165)],
2474
-    'spectral3': [(252, 141, 89), (255, 255, 191), (153, 213, 148)],
2475
-    'spectral4': [(215, 25, 28), (253, 174, 97), (171, 221, 164), (43, 131, 186)],
2476
-    'spectral5': [(215, 25, 28), (253, 174, 97), (255, 255, 191), (171, 221, 164), (43, 131, 186)],
2477
-    'spectral6': [(213, 62, 79), (252, 141, 89), (254, 224, 139), (230, 245, 152), (153, 213, 148), (50, 136, 189)],
2478
-    'spectral7': [(213, 62, 79), (252, 141, 89), (254, 224, 139), (255, 255, 191), (230, 245, 152), (153, 213, 148), (50, 136, 189)],
2479
-    'spectral8': [(213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (230, 245, 152), (171, 221, 164), (102, 194, 165), (50, 136, 189)],
2480
-    'spectral9': [(213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (230, 245, 152), (171, 221, 164), (102, 194, 165), (50, 136, 189)],
2481
-    'ylgn3': [(247, 252, 185), (173, 221, 142), (49, 163, 84)],
2482
-    'ylgn4': [(255, 255, 204), (194, 230, 153), (120, 198, 121), (35, 132, 67)],
2483
-    'ylgn5': [(255, 255, 204), (194, 230, 153), (120, 198, 121), (49, 163, 84), (0, 104, 55)],
2484
-    'ylgn6': [(255, 255, 204), (217, 240, 163), (173, 221, 142), (120, 198, 121), (49, 163, 84), (0, 104, 55)],
2485
-    'ylgn7': [(255, 255, 204), (217, 240, 163), (173, 221, 142), (120, 198, 121), (65, 171, 93), (35, 132, 67), (0, 90, 50)],
2486
-    'ylgn8': [(255, 255, 229), (247, 252, 185), (217, 240, 163), (173, 221, 142), (120, 198, 121), (65, 171, 93), (35, 132, 67), (0, 90, 50)],
2487
-    'ylgn9': [(255, 255, 229), (247, 252, 185), (217, 240, 163), (173, 221, 142), (120, 198, 121), (65, 171, 93), (35, 132, 67), (0, 104, 55), (0, 69, 41)],
2488
-    'ylgnbu3': [(237, 248, 177), (127, 205, 187), (44, 127, 184)],
2489
-    'ylgnbu4': [(255, 255, 204), (161, 218, 180), (65, 182, 196), (34, 94, 168)],
2490
-    'ylgnbu5': [(255, 255, 204), (161, 218, 180), (65, 182, 196), (44, 127, 184), (37, 52, 148)],
2491
-    'ylgnbu6': [(255, 255, 204), (199, 233, 180), (127, 205, 187), (65, 182, 196), (44, 127, 184), (37, 52, 148)],
2492
-    'ylgnbu7': [(255, 255, 204), (199, 233, 180), (127, 205, 187), (65, 182, 196), (29, 145, 192), (34, 94, 168), (12, 44, 132)],
2493
-    'ylgnbu8': [(255, 255, 217), (237, 248, 177), (199, 233, 180), (127, 205, 187), (65, 182, 196), (29, 145, 192), (34, 94, 168), (12, 44, 132)],
2494
-    'ylgnbu9': [(255, 255, 217), (237, 248, 177), (199, 233, 180), (127, 205, 187), (65, 182, 196), (29, 145, 192), (34, 94, 168), (37, 52, 148), (8, 29, 88)],
2495
-    'ylorbr3': [(255, 247, 188), (254, 196, 79), (217, 95, 14)],
2496
-    'ylorbr4': [(255, 255, 212), (254, 217, 142), (254, 153, 41), (204, 76, 2)],
2497
-    'ylorbr5': [(255, 255, 212), (254, 217, 142), (254, 153, 41), (217, 95, 14), (153, 52, 4)],
2498
-    'ylorbr6': [(255, 255, 212), (254, 227, 145), (254, 196, 79), (254, 153, 41), (217, 95, 14), (153, 52, 4)],
2499
-    'ylorbr7': [(255, 255, 212), (254, 227, 145), (254, 196, 79), (254, 153, 41), (236, 112, 20), (204, 76, 2), (140, 45, 4)],
2500
-    'ylorbr8': [(255, 255, 229), (255, 247, 188), (254, 227, 145), (254, 196, 79), (254, 153, 41), (236, 112, 20), (204, 76, 2), (140, 45, 4)],
2501
-    'ylorbr9': [(255, 255, 229), (255, 247, 188), (254, 227, 145), (254, 196, 79), (254, 153, 41), (236, 112, 20), (204, 76, 2), (153, 52, 4), (102, 37, 6)],
2502
-    'ylorrd3': [(255, 237, 160), (254, 178, 76), (240, 59, 32)],
2503
-    'ylorrd4': [(255, 255, 178), (254, 204, 92), (253, 141, 60), (227, 26, 28)],
2504
-    'ylorrd5': [(255, 255, 178), (254, 204, 92), (253, 141, 60), (240, 59, 32), (189, 0, 38)],
2505
-    'ylorrd6': [(255, 255, 178), (254, 217, 118), (254, 178, 76), (253, 141, 60), (240, 59, 32), (189, 0, 38)],
2506
-    'ylorrd7': [(255, 255, 178), (254, 217, 118), (254, 178, 76), (253, 141, 60), (252, 78, 42), (227, 26, 28), (177, 0, 38)],
2507
-    'ylorrd8': [(255, 255, 204), (255, 237, 160), (254, 217, 118), (254, 178, 76), (253, 141, 60), (252, 78, 42), (227, 26, 28), (177, 0, 38)],
2508
-}
2509
-
2510
-
2511
-if __name__ == '__main__':
2512
-    main()
2513 0
new file mode 100644
... ...
@@ -0,0 +1,9 @@
1
+'''Visualize dot graphs via the xdot format.'''
2
+
3
+__all__ = ['dot', 'ui']
4
+
5
+from . import dot
6
+from . import ui
7
+from .ui import DotWidget, DotWindow
8
+
9
+__author__ = "Jose Fonseca et al"
0 10
new file mode 100755
... ...
@@ -0,0 +1,86 @@
1
+#!/usr/bin/env python3
2
+#
3
+# Copyright 2008-2015 Jose Fonseca
4
+#
5
+# This program is free software: you can redistribute it and/or modify it
6
+# under the terms of the GNU Lesser General Public License as published
7
+# by the Free Software Foundation, either version 3 of the License, or
8
+# (at your option) any later version.
9
+#
10
+# This program is distributed in the hope that it will be useful,
11
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
+# GNU Lesser General Public License for more details.
14
+#
15
+# You should have received a copy of the GNU Lesser General Public License
16
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
+#
18
+import optparse
19
+import sys
20
+
21
+from .ui.window import DotWindow, Gtk
22
+
23
+
24
+class OptionParser(optparse.OptionParser):
25
+
26
+    def format_epilog(self, formatter):
27
+        # Prevent stripping the newlines in epilog message
28
+        # http://stackoverflow.com/questions/1857346/python-optparse-how-to-include-additional-info-in-usage-output
29
+        return self.epilog
30
+
31
+
32
+def main():
33
+
34
+    parser = OptionParser(
35
+        usage='\n\t%prog [file]',
36
+        epilog='''
37
+Shortcuts:
38
+  Up, Down, Left, Right     scroll
39
+  PageUp, +, =              zoom in
40
+  PageDown, -               zoom out
41
+  R                         reload dot file
42
+  F                         find
43
+  Q                         quit
44
+  P                         print
45
+  Escape                    halt animation
46
+  Ctrl-drag                 zoom in/out
47
+  Shift-drag                zooms an area
48
+'''
49
+    )
50
+    parser.add_option(
51
+        '-f', '--filter',
52
+        type='choice', choices=('dot', 'neato', 'twopi', 'circo', 'fdp'),
53
+        dest='filter', default='dot',
54
+        help='graphviz filter: dot, neato, twopi, circo, or fdp [default: %default]')
55
+    parser.add_option(
56
+        '-n', '--no-filter',
57
+        action='store_const', const=None, dest='filter',
58
+        help='assume input is already filtered into xdot format (use e.g. dot -Txdot)')
59
+    parser.add_option(
60
+        '-g', None,
61
+        action='store', dest='geometry',
62
+        help='default window size in form WxH')
63
+
64
+    (options, args) = parser.parse_args(sys.argv[1:])
65
+    if len(args) > 1:
66
+        parser.error('incorrect number of arguments')
67
+
68
+    width = height = 512
69
+    if options.geometry:
70
+        try:
71
+            width,height = (int(i) for i in options.geometry.split('x'))
72
+        except ValueError:
73
+            parser.error('invalid window geometry')
74
+
75
+    win = DotWindow(width=width, height=height)
76
+    win.connect('delete-event', Gtk.main_quit)
77
+    win.set_filter(options.filter)
78
+    if len(args) >= 1:
79
+        if args[0] == '-':
80
+            win.set_dotcode(sys.stdin.read())
81
+        else:
82
+            win.open_file(args[0])
83
+    Gtk.main()
84
+
85
+if __name__ == '__main__':
86
+    main()
0 87
new file mode 100644
... ...
@@ -0,0 +1 @@
1
+__all__ = ['lexer', 'parser']
0 2
new file mode 100644
... ...
@@ -0,0 +1,158 @@
1
+# Copyright 2008-2015 Jose Fonseca
2
+#
3
+# This program is free software: you can redistribute it and/or modify it
4
+# under the terms of the GNU Lesser General Public License as published
5
+# by the Free Software Foundation, either version 3 of the License, or
6
+# (at your option) any later version.
7
+#
8
+# This program is distributed in the hope that it will be useful,
9
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
+# GNU Lesser General Public License for more details.
12
+#
13
+# You should have received a copy of the GNU Lesser General Public License
14
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
+#
16
+import os
17
+import re
18
+
19
+from .scanner import DotScanner
20
+
21
+EOF = -1
22
+SKIP = -2
23
+
24
+ID = 0
25
+STR_ID = 1
26
+HTML_ID = 2
27
+
28
+
29
+class Token:
30
+
31
+    def __init__(self, type, text, line, col):
32
+        self.type = type
33
+        self.text = text
34
+        self.line = line
35
+        self.col = col
36
+
37
+
38
+class ParseError(Exception):
39
+
40
+    def __init__(self, msg=None, filename=None, line=None, col=None):
41
+        self.msg = msg
42
+        self.filename = filename
43
+        self.line = line
44
+        self.col = col
45
+
46
+    def __str__(self):
47
+        return ':'.join([str(part) for part in (self.filename, self.line, self.col, self.msg) if part != None])
48
+
49
+
50
+class Lexer:
51
+
52
+    # should be overriden by derived classes
53
+    scanner = None
54
+    tabsize = 8
55
+
56
+    newline_re = re.compile(br'\r\n?|\n')
57
+
58
+    def __init__(self, buf = None, pos = 0, filename = None, fp = None):
59
+        if fp is not None:
60
+            try:
61
+                fileno = fp.fileno()
62
+                length = os.path.getsize(fp.name)
63
+                import mmap
64
+            except:
65
+                # read whole file into memory
66
+                buf = fp.read()
67
+                pos = 0
68
+            else:
69
+                # map the whole file into memory
70
+                if length:
71
+                    # length must not be zero
72
+                    buf = mmap.mmap(fileno, length, access = mmap.ACCESS_READ)
73
+                    pos = os.lseek(fileno, 0, 1)
74
+                else:
75
+                    buf = b''
76
+                    pos = 0
77
+
78
+            if filename is None:
79
+                try:
80
+                    filename = fp.name
81
+                except AttributeError:
82
+                    filename = None
83
+
84
+        self.buf = buf
85
+        self.pos = pos
86
+        self.line = 1
87
+        self.col = 1
88
+        self.filename = filename
89
+
90
+    def __next__(self):
91
+        while True:
92
+            # save state
93
+            pos = self.pos
94
+            line = self.line
95
+            col = self.col
96
+
97
+            type, text, endpos = self.scanner.next(self.buf, pos)
98
+            assert isinstance(text, bytes)
99
+            assert pos + len(text) == endpos
100
+            self.consume(text)
101
+            type, text = self.filter(type, text)
102
+            self.pos = endpos
103
+
104
+            if type == SKIP:
105
+                continue
106
+            elif type is None:
107
+                msg = 'unexpected char %r' % (text,)
108
+                raise ParseError(msg, self.filename, line, col)
109
+            else:
110
+                break
111
+        return Token(type = type, text = text, line = line, col = col)
112
+
113
+    def consume(self, text):
114
+        # update line number
115
+        pos = 0
116
+        for mo in self.newline_re.finditer(text, pos):
117
+            self.line += 1
118
+            self.col = 1
119
+            pos = mo.end()
120
+
121
+        # update column number
122
+        while True:
123
+            tabpos = text.find(b'\t', pos)
124
+            if tabpos == -1:
125
+                break
126
+            self.col += tabpos - pos
127
+            self.col = ((self.col - 1)//self.tabsize + 1)*self.tabsize + 1
128
+            pos = tabpos + 1
129
+        self.col += len(text) - pos
130
+
131
+
132
+class DotLexer(Lexer):
133
+
134
+    scanner = DotScanner()
135
+
136
+    def filter(self, type, text):
137
+        # TODO: handle charset
138
+        if type == STR_ID:
139
+            text = text[1:-1]
140
+
141
+            # line continuations
142
+            text = text.replace(b'\\\r\n', b'')
143
+            text = text.replace(b'\\\r', b'')
144
+            text = text.replace(b'\\\n', b'')
145
+
146
+            # quotes
147
+            text = text.replace(b'\\"', b'"')
148
+
149
+            # layout engines recognize other escape codes (many non-standard)
150
+            # but we don't translate them here
151
+
152
+            type = ID
153
+
154
+        elif type == HTML_ID:
155
+            text = text[1:-1]
156
+            type = ID
157
+
158
+        return type, text
0 159
new file mode 100644
... ...
@@ -0,0 +1,575 @@
1
+# Copyright 2008-2015 Jose Fonseca
2
+#
3
+# This program is free software: you can redistribute it and/or modify it
4
+# under the terms of the GNU Lesser General Public License as published
5
+# by the Free Software Foundation, either version 3 of the License, or
6
+# (at your option) any later version.
7
+#
8
+# This program is distributed in the hope that it will be useful,
9
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
+# GNU Lesser General Public License for more details.
12
+#
13
+# You should have received a copy of the GNU Lesser General Public License
14
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
+#
16
+import colorsys
17
+import sys
18
+
19
+import gi
20
+gi.require_version('Gtk', '3.0')
21
+gi.require_version('PangoCairo', '1.0')
22
+
23
+from gi.repository import Gdk
24
+
25
+from .lexer import ParseError, DotLexer
26
+
27
+from ..ui.colours import brewer_colors
28
+from ..ui.pen import (Pen, BOLD, ITALIC, UNDERLINE, SUPERSCRIPT,
29
+                      SUBSCRIPT, STRIKE_THROUGH, OVERLINE)
30
+from ..ui.elements import (TextShape, ImageShape, EllipseShape,
31
+                           PolygonShape, LineShape, BezierShape,
32
+                           CompoundShape, Url, Jump, Element,
33
+                           Node, Edge, Graph)
34
+
35
+
36
+EOF = -1
37
+SKIP = -2
38
+
39
+ID = 0
40
+STR_ID = 1
41
+HTML_ID = 2
42
+EDGE_OP = 3
43
+
44
+LSQUARE = 4
45
+RSQUARE = 5
46
+LCURLY = 6
47
+RCURLY = 7
48
+COMMA = 8
49
+COLON = 9
50
+SEMI = 10
51
+EQUAL = 11
52
+PLUS = 12
53
+
54
+STRICT = 13
55
+GRAPH = 14
56
+DIGRAPH = 15
57
+NODE = 16
58
+EDGE = 17
59
+SUBGRAPH = 18
60
+
61
+
62
+class Parser:
63
+
64
+    def __init__(self, lexer):
65
+        self.lexer = lexer
66
+        self.lookahead = next(self.lexer)
67
+
68
+    def match(self, type):
69
+        if self.lookahead.type != type:
70
+            raise ParseError(
71
+                msg = 'unexpected token %r' % self.lookahead.text,
72
+                filename = self.lexer.filename,
73
+                line = self.lookahead.line,
74
+                col = self.lookahead.col)
75
+
76
+    def skip(self, type):
77
+        while self.lookahead.type != type:
78
+            if self.lookahead.type == EOF:
79
+                raise ParseError(
80
+                   msg = 'unexpected end of file',
81
+                   filename = self.lexer.filename,
82
+                   line = self.lookahead.line,
83
+                   col = self.lookahead.col)
84
+            self.consume()
85
+
86
+    def consume(self):
87
+        token = self.lookahead
88
+        self.lookahead = next(self.lexer)
89
+        return token
90
+
91
+
92
+class XDotAttrParser:
93
+    """Parser for xdot drawing attributes.
94
+    See also:
95
+    - http://www.graphviz.org/doc/info/output.html#d:xdot
96
+    """
97
+
98
+    def __init__(self, parser, buf):
99
+        self.parser = parser
100
+        self.buf = buf
101
+        self.pos = 0
102
+
103
+        self.pen = Pen()
104
+        self.shapes = []
105
+
106
+    def __bool__(self):
107
+        return self.pos < len(self.buf)
108
+
109
+    def read_code(self):
110
+        pos = self.buf.find(b" ", self.pos)
111
+        res = self.buf[self.pos:pos]
112
+        self.pos = pos + 1
113
+        self.skip_space()
114
+        res = res.decode('utf-8')
115
+        return res
116
+
117
+    def skip_space(self):
118
+        while self.pos < len(self.buf) and self.buf[self.pos : self.pos + 1].isspace():
119
+            self.pos += 1
120
+
121
+    def read_int(self):
122
+        return int(self.read_code())
123
+
124
+    def read_float(self):
125
+        return float(self.read_code())
126
+
127
+    def read_point(self):
128
+        x = self.read_float()
129
+        y = self.read_float()
130
+        return self.transform(x, y)
131
+
132
+    def read_text(self):
133
+        num = self.read_int()
134
+        pos = self.buf.find(b"-", self.pos) + 1
135
+        self.pos = pos + num
136
+        res = self.buf[pos:self.pos]
137
+        self.skip_space()
138
+        res = res.decode('utf-8')
139
+        return res
140
+
141
+    def read_polygon(self):
142
+        n = self.read_int()
143
+        p = []
144
+        for i in range(n):
145
+            x, y = self.read_point()
146
+            p.append((x, y))
147
+        return p
148
+
149
+    def read_color(self):
150
+        # See http://www.graphviz.org/doc/info/attrs.html#k:color
151
+        c = self.read_text()
152
+        c1 = c[:1]
153
+        if c1 == '#':
154
+            hex2float = lambda h: float(int(h, 16)/255.0)
155
+            r = hex2float(c[1:3])
156
+            g = hex2float(c[3:5])
157
+            b = hex2float(c[5:7])
158
+            try:
159
+                a = hex2float(c[7:9])
160
+            except (IndexError, ValueError):
161
+                a = 1.0
162
+            return r, g, b, a
163
+        elif c1.isdigit() or c1 == ".":
164
+            # "H,S,V" or "H S V" or "H, S, V" or any other variation
165
+            h, s, v = map(float, c.replace(",", " ").split())
166
+            r, g, b = colorsys.hsv_to_rgb(h, s, v)
167
+            a = 1.0
168
+            return r, g, b, a
169
+        elif c1 == "[" or c1 == "(":
170
+            sys.stderr.write('warning: color gradients not supported yet\n')
171
+            return None
172
+        else:
173
+            return self.lookup_color(c)
174
+
175
+    def lookup_color(self, c):
176
+        try:
177
+            color = Gdk.color_parse(c)
178
+        except ValueError:
179
+            pass
180
+        else:
181
+            s = 1.0/65535.0
182
+            r = color.red*s
183
+            g = color.green*s
184
+            b = color.blue*s
185
+            a = 1.0
186
+            return r, g, b, a
187
+
188
+        try:
189
+            dummy, scheme, index = c.split('/')
190
+            r, g, b = brewer_colors[scheme][int(index)]
191
+        except (ValueError, KeyError):
192
+            pass
193
+        else:
194
+            s = 1.0/255.0
195
+            r = r*s
196
+            g = g*s
197
+            b = b*s
198
+            a = 1.0
199
+            return r, g, b, a
200
+
201
+        sys.stderr.write("warning: unknown color '%s'\n" % c)
202
+        return None
203
+
204
+    def parse(self):
205
+        s = self
206
+
207
+        while s:
208
+            op = s.read_code()
209
+            if op == "c":
210
+                color = s.read_color()
211
+                if color is not None:
212
+                    self.handle_color(color, filled=False)
213
+            elif op == "C":
214
+                color = s.read_color()
215
+                if color is not None:
216
+                    self.handle_color(color, filled=True)
217
+            elif op == "S":
218
+                # http://www.graphviz.org/doc/info/attrs.html#k:style
219
+                style = s.read_text()
220
+                if style.startswith("setlinewidth("):
221
+                    lw = style.split("(")[1].split(")")[0]
222
+                    lw = float(lw)
223
+                    self.handle_linewidth(lw)
224
+                elif style in ("solid", "dashed", "dotted"):
225
+                    self.handle_linestyle(style)
226
+            elif op == "F":
227
+                size = s.read_float()
228
+                name = s.read_text()
229
+                self.handle_font(size, name)
230
+            elif op == "T":
231
+                x, y = s.read_point()
232
+                j = s.read_int()
233
+                w = s.read_float()
234
+                t = s.read_text()
235
+                self.handle_text(x, y, j, w, t)
236
+            elif op == "t":
237
+                f = s.read_int()
238
+                self.handle_font_characteristics(f)
239
+            elif op == "E":
240
+                x0, y0 = s.read_point()
241
+                w = s.read_float()
242
+                h = s.read_float()
243
+                self.handle_ellipse(x0, y0, w, h, filled=True)
244
+            elif op == "e":
245
+                x0, y0 = s.read_point()
246
+                w = s.read_float()
247
+                h = s.read_float()
248
+                self.handle_ellipse(x0, y0, w, h, filled=False)
249
+            elif op == "L":
250
+                points = self.read_polygon()
251
+                self.handle_line(points)
252
+            elif op == "B":
253
+                points = self.read_polygon()
254
+                self.handle_bezier(points, filled=False)
255
+            elif op == "b":
256
+                points = self.read_polygon()
257
+                self.handle_bezier(points, filled=True)
258
+            elif op == "P":
259
+                points = self.read_polygon()
260
+                self.handle_polygon(points, filled=True)
261
+            elif op == "p":
262
+                points = self.read_polygon()
263
+                self.handle_polygon(points, filled=False)
264
+            elif op == "I":
265
+                x0, y0 = s.read_point()
266
+                w = s.read_float()
267
+                h = s.read_float()
268
+                path = s.read_text()
269
+                self.handle_image(x0, y0, w, h, path)
270
+            else:
271
+                sys.stderr.write("error: unknown xdot opcode '%s'\n" % op)
272
+                sys.exit(1)
273
+
274
+        return self.shapes
275
+
276
+    def transform(self, x, y):
277
+        return self.parser.transform(x, y)
278
+
279
+    def handle_color(self, color, filled=False):
280
+        if filled:
281
+            self.pen.fillcolor = color
282
+        else:
283
+            self.pen.color = color
284
+
285
+    def handle_linewidth(self, linewidth):
286
+        self.pen.linewidth = linewidth
287
+
288
+    def handle_linestyle(self, style):
289
+        if style == "solid":
290
+            self.pen.dash = ()
291
+        elif style == "dashed":
292
+            self.pen.dash = (6, )       # 6pt on, 6pt off
293
+        elif style == "dotted":
294
+            self.pen.dash = (2, 4)       # 2pt on, 4pt off
295
+
296
+    def handle_font(self, size, name):
297
+        self.pen.fontsize = size
298
+        self.pen.fontname = name
299
+
300
+    def handle_font_characteristics(self, flags):
301
+        self.pen.bold = bool(flags & BOLD)
302
+        self.pen.italic = bool(flags & ITALIC)
303
+        self.pen.underline = bool(flags & UNDERLINE)
304
+        self.pen.superscript = bool(flags & SUPERSCRIPT)
305
+        self.pen.subscript = bool(flags & SUBSCRIPT)
306
+        self.pen.strikethrough = bool(flags & STRIKE_THROUGH)
307
+        self.pen.overline = bool(flags & OVERLINE)
308
+        if self.pen.overline:
309
+            sys.stderr.write('warning: overlined text not supported yet\n')
310
+
311
+    def handle_text(self, x, y, j, w, t):
312
+        self.shapes.append(TextShape(self.pen, x, y, j, w, t))
313
+
314
+    def handle_ellipse(self, x0, y0, w, h, filled=False):
315
+        if filled:
316
+            # xdot uses this to mean "draw a filled shape with an outline"
317
+            self.shapes.append(EllipseShape(self.pen, x0, y0, w, h, filled=True))
318
+        self.shapes.append(EllipseShape(self.pen, x0, y0, w, h))
319
+
320
+    def handle_image(self, x0, y0, w, h, path):
321
+        self.shapes.append(ImageShape(self.pen, x0, y0, w, h, path))
322
+
323
+    def handle_line(self, points):
324
+        self.shapes.append(LineShape(self.pen, points))
325
+
326
+    def handle_bezier(self, points, filled=False):
327
+        if filled:
328
+            # xdot uses this to mean "draw a filled shape with an outline"
329
+            self.shapes.append(BezierShape(self.pen, points, filled=True))
330
+        self.shapes.append(BezierShape(self.pen, points))
331
+
332
+    def handle_polygon(self, points, filled=False):
333
+        if filled:
334
+            # xdot uses this to mean "draw a filled shape with an outline"
335
+            self.shapes.append(PolygonShape(self.pen, points, filled=True))
336
+        self.shapes.append(PolygonShape(self.pen, points))
337
+
338
+
339
+class DotParser(Parser):
340
+
341
+    def __init__(self, lexer):
342
+        Parser.__init__(self, lexer)
343
+        self.graph_attrs = {}
344
+        self.node_attrs = {}
345
+        self.edge_attrs = {}
346
+
347
+    def parse(self):
348
+        self.parse_graph()
349
+        self.match(EOF)
350
+
351
+    def parse_graph(self):
352
+        if self.lookahead.type == STRICT:
353
+            self.consume()
354
+        self.skip(LCURLY)
355
+        self.consume()
356
+        while self.lookahead.type != RCURLY:
357
+            self.parse_stmt()
358
+        self.consume()
359
+
360
+    def parse_subgraph(self):
361
+        id = None
362
+        if self.lookahead.type == SUBGRAPH:
363
+            self.consume()
364
+            if self.lookahead.type == ID:
365
+                id = self.lookahead.text
366
+                self.consume()
367
+        if self.lookahead.type == LCURLY:
368
+            self.consume()
369
+            while self.lookahead.type != RCURLY:
370
+                self.parse_stmt()
371
+            self.consume()
372
+        return id
373
+
374
+    def parse_stmt(self):
375
+        if self.lookahead.type == GRAPH:
376
+            self.consume()
377
+            attrs = self.parse_attrs()
378
+            self.graph_attrs.update(attrs)
379
+            self.handle_graph(attrs)
380
+        elif self.lookahead.type == NODE:
381
+            self.consume()
382
+            self.node_attrs.update(self.parse_attrs())
383
+        elif self.lookahead.type == EDGE:
384
+            self.consume()
385
+            self.edge_attrs.update(self.parse_attrs())
386
+        elif self.lookahead.type in (SUBGRAPH, LCURLY):
387
+            self.parse_subgraph()
388
+        else:
389
+            id = self.parse_node_id()
390
+            if self.lookahead.type == EDGE_OP:
391
+                self.consume()
392
+                node_ids = [id, self.parse_node_id()]
393
+                while self.lookahead.type == EDGE_OP:
394
+                    node_ids.append(self.parse_node_id())
395
+                attrs = self.parse_attrs()
396
+                for i in range(0, len(node_ids) - 1):
397
+                    self.handle_edge(node_ids[i], node_ids[i + 1], attrs)
398
+            elif self.lookahead.type == EQUAL:
399
+                self.consume()
400
+                self.parse_id()
401
+            else:
402
+                attrs = self.parse_attrs()
403
+                self.handle_node(id, attrs)
404
+        if self.lookahead.type == SEMI:
405
+            self.consume()
406
+
407
+    def parse_attrs(self):
408
+        attrs = {}
409
+        while self.lookahead.type == LSQUARE:
410
+            self.consume()
411
+            while self.lookahead.type != RSQUARE:
412
+                name, value = self.parse_attr()
413
+                name = name.decode('utf-8')
414
+                attrs[name] = value
415
+                if self.lookahead.type == COMMA:
416
+                    self.consume()
417
+            self.consume()
418
+        return attrs
419
+
420
+    def parse_attr(self):
421
+        name = self.parse_id()
422
+        if self.lookahead.type == EQUAL:
423
+            self.consume()
424
+            value = self.parse_id()
425
+        else:
426
+            value = b'true'
427
+        return name, value
428
+
429
+    def parse_node_id(self):
430
+        node_id = self.parse_id()
431
+        if self.lookahead.type == COLON:
432
+            self.consume()
433
+            port = self.parse_id()
434
+            if self.lookahead.type == COLON:
435
+                self.consume()
436
+                compass_pt = self.parse_id()
437
+            else:
438
+                compass_pt = None
439
+        else:
440
+            port = None
441
+            compass_pt = None
442
+        # XXX: we don't really care about port and compass point values when parsing xdot
443
+        return node_id
444
+
445
+    def parse_id(self):
446
+        self.match(ID)
447
+        id = self.lookahead.text
448
+        self.consume()
449
+        return id
450
+
451
+    def handle_graph(self, attrs):
452
+        pass
453
+
454
+    def handle_node(self, id, attrs):
455
+        pass
456
+
457
+    def handle_edge(self, src_id, dst_id, attrs):
458
+        pass
459
+
460
+
461
+class XDotParser(DotParser):
462
+
463
+    XDOTVERSION = '1.7'
464
+
465
+    def __init__(self, xdotcode):
466
+        lexer = DotLexer(buf = xdotcode)
467
+        DotParser.__init__(self, lexer)
468
+
469
+        self.nodes = []
470
+        self.edges = []
471
+        self.shapes = []
472
+        self.node_by_name = {}
473
+        self.top_graph = True
474
+        self.width = 0
475
+        self.height = 0
476
+
477
+    def handle_graph(self, attrs):
478
+        if self.top_graph:
479
+            # Check xdot version
480
+            try:
481
+                xdotversion = attrs['xdotversion']
482
+            except KeyError:
483
+                pass
484
+            else:
485
+                if float(xdotversion) > float(self.XDOTVERSION):
486
+                    sys.stderr.write('warning: xdot version %s, but supported is %s\n' % (xdotversion, self.XDOTVERSION))
487
+
488
+            # Parse bounding box
489
+            try:
490
+                bb = attrs['bb']
491
+            except KeyError:
492
+                return
493
+
494
+            if bb:
495
+                xmin, ymin, xmax, ymax = map(float, bb.split(b","))
496
+
497
+                self.xoffset = -xmin
498
+                self.yoffset = -ymax
499
+                self.xscale = 1.0
500
+                self.yscale = -1.0
501
+                # FIXME: scale from points to pixels
502
+
503
+                self.width  = max(xmax - xmin, 1)
504
+                self.height = max(ymax - ymin, 1)
505
+
506
+                self.top_graph = False
507
+
508
+        for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
509
+            if attr in attrs:
510
+                parser = XDotAttrParser(self, attrs[attr])
511
+                self.shapes.extend(parser.parse())
512
+
513
+    def handle_node(self, id, attrs):
514
+        try:
515
+            pos = attrs['pos']
516
+        except KeyError:
517
+            return
518
+
519
+        x, y = self.parse_node_pos(pos)
520
+        w = float(attrs.get('width', 0))*72
521
+        h = float(attrs.get('height', 0))*72
522
+        shapes = []
523
+        for attr in ("_draw_", "_ldraw_"):
524
+            if attr in attrs:
525
+                parser = XDotAttrParser(self, attrs[attr])
526
+                shapes.extend(parser.parse())
527
+        url = attrs.get('URL', None)
528
+        node = Node(id, x, y, w, h, shapes, url)
529
+        self.node_by_name[id] = node
530
+        if shapes:
531
+            self.nodes.append(node)
532
+
533
+    def handle_edge(self, src_id, dst_id, attrs):
534
+        try:
535
+            pos = attrs['pos']
536
+        except KeyError:
537
+            return
538
+
539
+        points = self.parse_edge_pos(pos)
540
+        shapes = []
541
+        for attr in ("_draw_", "_ldraw_", "_hdraw_", "_tdraw_", "_hldraw_", "_tldraw_"):
542
+            if attr in attrs:
543
+                parser = XDotAttrParser(self, attrs[attr])
544
+                shapes.extend(parser.parse())
545
+        if shapes:
546
+            src = self.node_by_name[src_id]
547
+            dst = self.node_by_name[dst_id]
548
+            self.edges.append(Edge(src, dst, points, shapes))
549
+
550
+    def parse(self):
551
+        DotParser.parse(self)
552
+        return Graph(self.width, self.height, self.shapes, self.nodes, self.edges)
553
+
554
+    def parse_node_pos(self, pos):
555
+        x, y = pos.split(b",")
556
+        return self.transform(float(x), float(y))
557
+
558
+    def parse_edge_pos(self, pos):
559
+        points = []
560
+        for entry in pos.split(b' '):
561
+            fields = entry.split(b',')
562
+            try:
563
+                x, y = fields
564
+            except ValueError:
565
+                # TODO: handle start/end points
566
+                continue
567
+            else:
568
+                points.append(self.transform(float(x), float(y)))
569
+        return points
570
+
571
+    def transform(self, x, y):
572
+        # XXX: this is not the right place for this code
573
+        x = (x + self.xoffset)*self.xscale
574
+        y = (y + self.yoffset)*self.yscale
575
+        return x, y
0 576
new file mode 100644
... ...
@@ -0,0 +1,129 @@
1
+# Copyright 2008-2015 Jose Fonseca
2
+#
3
+# This program is free software: you can redistribute it and/or modify it
4
+# under the terms of the GNU Lesser General Public License as published
5
+# by the Free Software Foundation, either version 3 of the License, or
6
+# (at your option) any later version.
7
+#
8
+# This program is distributed in the hope that it will be useful,
9
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
+# GNU Lesser General Public License for more details.
12
+#
13
+# You should have received a copy of the GNU Lesser General Public License
14
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
+#
16
+import re
17
+
18
+EOF = -1
19
+SKIP = -2
20
+
21
+ID = 0
22
+STR_ID = 1
23
+HTML_ID = 2
24
+EDGE_OP = 3
25
+
26
+LSQUARE = 4
27
+RSQUARE = 5
28
+LCURLY = 6
29
+RCURLY = 7
30
+COMMA = 8
31
+COLON = 9
32
+SEMI = 10
33
+EQUAL = 11
34
+PLUS = 12
35
+
36
+STRICT = 13
37
+GRAPH = 14
38
+DIGRAPH = 15
39
+NODE = 16
40
+EDGE = 17
41
+SUBGRAPH = 18
42
+
43
+
44
+class Scanner:
45
+    """Stateless scanner."""
46
+
47
+    # should be overriden by derived classes
48
+    tokens = []
49
+    symbols = {}
50
+    literals = {}
51
+    ignorecase = False
52
+
53
+    def __init__(self):
54
+        flags = re.DOTALL
55
+        if self.ignorecase:
56
+            flags |= re.IGNORECASE
57
+        self.tokens_re = re.compile(
58
+            b'|'.join([b'(' + regexp + b')' for type, regexp, test_lit in self.tokens]),
59
+             flags
60
+        )
61
+
62
+    def next(self, buf, pos):
63
+        if pos >= len(buf):
64
+            return EOF, b'', pos
65
+        mo = self.tokens_re.match(buf, pos)
66
+        if mo:
67
+            text = mo.group()
68
+            type, regexp, test_lit = self.tokens[mo.lastindex - 1]
69
+            pos = mo.end()
70
+            if test_lit:
71
+                type = self.literals.get(text, type)
72
+            return type, text, pos
73
+        else:
74
+            c = buf[pos : pos + 1]
75
+            return self.symbols.get(c, None), c, pos + 1
76
+
77
+
78
+class DotScanner(Scanner):
79
+
80
+    # token regular expression table
81
+    tokens = [
82
+        # whitespace and comments
83
+        (SKIP,
84
+            br'[ \t\f\r\n\v]+|'
85
+            br'//[^\r\n]*|'
86
+            br'/\*.*?\*/|'
87
+            br'#[^\r\n]*',
88
+        False),
89
+
90
+        # Alphanumeric IDs
91
+        (ID, br'[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*', True),
92
+
93
+        # Numeric IDs
94
+        (ID, br'-?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)', False),
95
+
96
+        # String IDs
97
+        (STR_ID, br'"[^"\\]*(?:\\.[^"\\]*)*"', False),
98
+
99
+        # HTML IDs
100
+        (HTML_ID, br'<[^<>]*(?:<[^<>]*>[^<>]*)*>', False),
101
+
102
+        # Edge operators
103
+        (EDGE_OP, br'-[>-]', False),
104
+    ]
105
+
106
+    # symbol table
107
+    symbols = {
108
+        b'[': LSQUARE,
109
+        b']': RSQUARE,
110
+        b'{': LCURLY,
111
+        b'}': RCURLY,
112
+        b',': COMMA,
113
+        b':': COLON,
114
+        b';': SEMI,
115
+        b'=': EQUAL,
116
+        b'+': PLUS,
117
+    }
118
+
119
+    # literal table
120
+    literals = {
121
+        b'strict': STRICT,
122
+        b'graph': GRAPH,
123
+        b'digraph': DIGRAPH,
124
+        b'node': NODE,
125
+        b'edge': EDGE,
126
+        b'subgraph': SUBGRAPH,
127
+    }
128
+
129
+    ignorecase = True
0 130
new file mode 100644
... ...
@@ -0,0 +1,3 @@
1
+__all__ = ['actions', 'animation', 'colours', 'elements', 'pen', 'window']
2
+
3
+from .window import DotWidget, DotWindow
0 4
new file mode 100644
... ...
@@ -0,0 +1,139 @@
1
+# Copyright 2008-2015 Jose Fonseca
2
+#
3
+# This program is free software: you can redistribute it and/or modify it
4
+# under the terms of the GNU Lesser General Public License as published
5
+# by the Free Software Foundation, either version 3 of the License, or
6
+# (at your option) any later version.
7
+#
8
+# This program is distributed in the hope that it will be useful,
9
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
+# GNU Lesser General Public License for more details.
12
+#
13
+# You should have received a copy of the GNU Lesser General Public License
14
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
+#
16
+import gi
17
+gi.require_version('Gtk', '3.0')
18
+gi.require_version('PangoCairo', '1.0')
19
+
20
+from gi.repository import Gdk
21
+
22
+
23
+class DragAction(object):
24
+
25
+    def __init__(self, dot_widget):
26
+        self.dot_widget = dot_widget
27
+
28
+    def on_button_press(self, event):
29
+        self.startmousex = self.prevmousex = event.x
30
+        self.startmousey = self.prevmousey = event.y
31
+        self.start()
32
+
33
+    def on_motion_notify(self, event):
34
+        if event.is_hint:
35
+            window, x, y, state = event.window.get_device_position(event.device)
36
+        else:
37
+            x, y, state = event.x, event.y, event.state
38
+        deltax = self.prevmousex - x
39
+        deltay = self.prevmousey - y
40
+        self.drag(deltax, deltay)
41
+        self.prevmousex = x
42
+        self.prevmousey = y
43
+
44
+    def on_button_release(self, event):
45
+        self.stopmousex = event.x
46
+        self.stopmousey = event.y
47
+        self.stop()
48
+
49
+    def draw(self, cr):
50
+        pass
51
+
52
+    def start(self):
53
+        pass
54
+
55
+    def drag(self, deltax, deltay):
56
+        pass
57
+
58
+    def stop(self):
59
+        pass
60
+
61
+    def abort(self):
62
+        pass
63
+
64
+
65
+class NullAction(DragAction):
66
+
67
+    def on_motion_notify(self, event):
68
+        if event.is_hint:
69
+            window, x, y, state = event.window.get_device_position(event.device)
70
+        else:
71
+            x, y, state = event.x, event.y, event.state
72
+        dot_widget = self.dot_widget
73
+        item = dot_widget.get_url(x, y)
74
+        if item is None:
75
+            item = dot_widget.get_jump(x, y)
76
+        if item is not None:
77
+            dot_widget.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2))
78
+            dot_widget.set_highlight(item.highlight)
79
+        else:
80
+            dot_widget.get_window().set_cursor(None)
81
+            dot_widget.set_highlight(None)
82
+
83
+
84
+class PanAction(DragAction):
85
+
86
+    def start(self):
87
+        self.dot_widget.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.FLEUR))
88
+
89
+    def drag(self, deltax, deltay):
90
+        self.dot_widget.x += deltax / self.dot_widget.zoom_ratio
91
+        self.dot_widget.y += deltay / self.dot_widget.zoom_ratio
92
+        self.dot_widget.queue_draw()
93
+
94
+    def stop(self):
95
+        self.dot_widget.get_window().set_cursor(None)
96
+
97
+    abort = stop
98
+
99
+
100
+class ZoomAction(DragAction):
101
+
102
+    def drag(self, deltax, deltay):
103
+        self.dot_widget.zoom_ratio *= 1.005 ** (deltax + deltay)
104
+        self.dot_widget.zoom_to_fit_on_resize = False
105
+        self.dot_widget.queue_draw()
106
+
107
+    def stop(self):
108
+        self.dot_widget.queue_draw()
109
+
110
+
111
+class ZoomAreaAction(DragAction):
112
+
113
+    def drag(self, deltax, deltay):
114
+        self.dot_widget.queue_draw()
115
+
116
+    def draw(self, cr):
117
+        cr.save()
118
+        cr.set_source_rgba(.5, .5, 1.0, 0.25)
119
+        cr.rectangle(self.startmousex, self.startmousey,
120
+                     self.prevmousex - self.startmousex,
121
+                     self.prevmousey - self.startmousey)
122
+        cr.fill()
123
+        cr.set_source_rgba(.5, .5, 1.0, 1.0)
124
+        cr.set_line_width(1)
125
+        cr.rectangle(self.startmousex - .5, self.startmousey - .5,
126
+                     self.prevmousex - self.startmousex + 1,
127
+                     self.prevmousey - self.startmousey + 1)
128
+        cr.stroke()
129
+        cr.restore()
130
+
131
+    def stop(self):
132
+        x1, y1 = self.dot_widget.window2graph(self.startmousex,
133
+                                              self.startmousey)
134
+        x2, y2 = self.dot_widget.window2graph(self.stopmousex,
135
+                                              self.stopmousey)
136
+        self.dot_widget.zoom_to_area(x1, y1, x2, y2)
137
+
138
+    def abort(self):
139
+        self.dot_widget.queue_draw()
0 140
new file mode 100644
... ...
@@ -0,0 +1,123 @@
1
+# Copyright 2008-2015 Jose Fonseca
2
+#
3
+# This program is free software: you can redistribute it and/or modify it
4
+# under the terms of the GNU Lesser General Public License as published
5
+# by the Free Software Foundation, either version 3 of the License, or
6
+# (at your option) any later version.
7
+#
8
+# This program is distributed in the hope that it will be useful,
9
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
+# GNU Lesser General Public License for more details.
12
+#
13
+# You should have received a copy of the GNU Lesser General Public License
14
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
+#
16
+import math
17
+import time
18
+
19
+import gi
20
+gi.require_version('Gtk', '3.0')
21
+gi.require_version('PangoCairo', '1.0')
22
+
23
+from gi.repository import GLib
24
+
25
+
26
+class Animation(object):
27
+
28
+    step = 0.03 # seconds
29
+
30
+    def __init__(self, dot_widget):
31
+        self.dot_widget = dot_widget
32
+        self.timeout_id = None
33
+
34
+    def start(self):
35
+        self.timeout_id = GLib.timeout_add(int(self.step * 1000), self.__real_tick)
36
+
37
+    def stop(self):
38
+        self.dot_widget.animation = NoAnimation(self.dot_widget)
39
+        if self.timeout_id is not None:
40
+            GLib.source_remove(self.timeout_id)
41
+            self.timeout_id = None
42
+
43
+    def __real_tick(self):
44
+        try:
45
+            if not self.tick():
46
+                self.stop()
47
+                return False
48
+        except e:
49
+            self.stop()
50
+            raise e
51
+        return True
52
+
53
+    def tick(self):
54
+        return False
55
+
56
+
57
+class NoAnimation(Animation):
58
+
59
+    def start(self):
60
+        pass
61
+
62
+    def stop(self):
63
+        pass
64
+
65
+
66
+class LinearAnimation(Animation):
67
+
68
+    duration = 0.6
69
+
70
+    def start(self):
71
+        self.started = time.time()
72
+        Animation.start(self)
73
+
74
+    def tick(self):
75
+        t = (time.time() - self.started) / self.duration
76
+        self.animate(max(0, min(t, 1)))
77
+        return (t < 1)
78
+
79
+    def animate(self, t):
80
+        pass
81
+
82
+
83
+class MoveToAnimation(LinearAnimation):
84
+
85
+    def __init__(self, dot_widget, target_x, target_y):
86
+        Animation.__init__(self, dot_widget)
87
+        self.source_x = dot_widget.x
88
+        self.source_y = dot_widget.y
89
+        self.target_x = target_x
90
+        self.target_y = target_y
91
+
92
+    def animate(self, t):
93
+        sx, sy = self.source_x, self.source_y
94
+        tx, ty = self.target_x, self.target_y
95
+        self.dot_widget.x = tx * t + sx * (1-t)
96
+        self.dot_widget.y = ty * t + sy * (1-t)
97
+        self.dot_widget.queue_draw()
98
+
99
+
100
+class ZoomToAnimation(MoveToAnimation):
101
+
102
+    def __init__(self, dot_widget, target_x, target_y):
103
+        MoveToAnimation.__init__(self, dot_widget, target_x, target_y)
104
+        self.source_zoom = dot_widget.zoom_ratio
105
+        self.target_zoom = self.source_zoom
106
+        self.extra_zoom = 0
107
+
108
+        middle_zoom = 0.5 * (self.source_zoom + self.target_zoom)
109
+
110
+        distance = math.hypot(self.source_x - self.target_x,
111
+                              self.source_y - self.target_y)
112
+        rect = self.dot_widget.get_allocation()
113
+        visible = min(rect.width, rect.height) / self.dot_widget.zoom_ratio
114
+        visible *= 0.9
115
+        if distance > 0:
116
+            desired_middle_zoom = visible / distance
117
+            self.extra_zoom = min(0, 4 * (desired_middle_zoom - middle_zoom))
118
+
119
+    def animate(self, t):
120
+        a, b, c = self.source_zoom, self.extra_zoom, self.target_zoom
121
+        self.dot_widget.zoom_ratio = c*t + b*t*(1-t) + a*(1-t)
122
+        self.dot_widget.zoom_to_fit_on_resize = False
123
+        MoveToAnimation.animate(self, t)
0 124
new file mode 100644
... ...
@@ -0,0 +1,305 @@
1
+# Apache-Style Software License for ColorBrewer software and ColorBrewer Color
2
+# Schemes, Version 1.1
3
+#
4
+# Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State
5
+# University. All rights reserved.
6
+#
7
+# Redistribution and use in source and binary forms, with or without
8
+# modification, are permitted provided that the following conditions are met:
9
+#
10
+#    1. Redistributions as source code must retain the above copyright notice,
11
+#    this list of conditions and the following disclaimer.
12
+#
13
+#    2. The end-user documentation included with the redistribution, if any,
14
+#    must include the following acknowledgment:
15
+#
16
+#       This product includes color specifications and designs developed by
17
+#       Cynthia Brewer (http://colorbrewer.org/).
18
+#
19
+#    Alternately, this acknowledgment may appear in the software itself, if and
20
+#    wherever such third-party acknowledgments normally appear.
21
+#
22
+#    3. The name "ColorBrewer" must not be used to endorse or promote products
23
+#    derived from this software without prior written permission. For written
24
+#    permission, please contact Cynthia Brewer at cbrewer@psu.edu.
25
+#
26
+#    4. Products derived from this software may not be called "ColorBrewer",
27
+#    nor may "ColorBrewer" appear in their name, without prior written
28
+#    permission of Cynthia Brewer.
29
+#
30
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES,
31
+# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
32
+# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CYNTHIA
33
+# BREWER, MARK HARROWER, OR THE PENNSYLVANIA STATE UNIVERSITY BE LIABLE FOR ANY
34
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
35
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
36
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
37
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
38
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
39
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40
+brewer_colors = {
41
+    'accent3': [(127, 201, 127), (190, 174, 212), (253, 192, 134)],
42
+    'accent4': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153)],
43
+    'accent5': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176)],
44
+    'accent6': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176), (240, 2, 127)],
45
+    'accent7': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176), (240, 2, 127), (191, 91, 23)],
46
+    'accent8': [(127, 201, 127), (190, 174, 212), (253, 192, 134), (255, 255, 153), (56, 108, 176), (240, 2, 127), (191, 91, 23), (102, 102, 102)],
47
+    'blues3': [(222, 235, 247), (158, 202, 225), (49, 130, 189)],
48
+    'blues4': [(239, 243, 255), (189, 215, 231), (107, 174, 214), (33, 113, 181)],
49
+    'blues5': [(239, 243, 255), (189, 215, 231), (107, 174, 214), (49, 130, 189), (8, 81, 156)],
50
+    'blues6': [(239, 243, 255), (198, 219, 239), (158, 202, 225), (107, 174, 214), (49, 130, 189), (8, 81, 156)],
51
+    'blues7': [(239, 243, 255), (198, 219, 239), (158, 202, 225), (107, 174, 214), (66, 146, 198), (33, 113, 181), (8, 69, 148)],
52
+    'blues8': [(247, 251, 255), (222, 235, 247), (198, 219, 239), (158, 202, 225), (107, 174, 214), (66, 146, 198), (33, 113, 181), (8, 69, 148)],
53
+    'blues9': [(247, 251, 255), (222, 235, 247), (198, 219, 239), (158, 202, 225), (107, 174, 214), (66, 146, 198), (33, 113, 181), (8, 81, 156), (8, 48, 107)],
54
+    'brbg10': [(84, 48, 5), (0, 60, 48), (140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (199, 234, 229), (128, 205, 193), (53, 151, 143), (1, 102, 94)],
55
+    'brbg11': [(84, 48, 5), (1, 102, 94), (0, 60, 48), (140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (245, 245, 245), (199, 234, 229), (128, 205, 193), (53, 151, 143)],
56
+    'brbg3': [(216, 179, 101), (245, 245, 245), (90, 180, 172)],
57
+    'brbg4': [(166, 97, 26), (223, 194, 125), (128, 205, 193), (1, 133, 113)],
58
+    'brbg5': [(166, 97, 26), (223, 194, 125), (245, 245, 245), (128, 205, 193), (1, 133, 113)],
59
+    'brbg6': [(140, 81, 10), (216, 179, 101), (246, 232, 195), (199, 234, 229), (90, 180, 172), (1, 102, 94)],
60
+    'brbg7': [(140, 81, 10), (216, 179, 101), (246, 232, 195), (245, 245, 245), (199, 234, 229), (90, 180, 172), (1, 102, 94)],
61
+    'brbg8': [(140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (199, 234, 229), (128, 205, 193), (53, 151, 143), (1, 102, 94)],
62
+    'brbg9': [(140, 81, 10), (191, 129, 45), (223, 194, 125), (246, 232, 195), (245, 245, 245), (199, 234, 229), (128, 205, 193), (53, 151, 143), (1, 102, 94)],
63
+    'bugn3': [(229, 245, 249), (153, 216, 201), (44, 162, 95)],
64
+    'bugn4': [(237, 248, 251), (178, 226, 226), (102, 194, 164), (35, 139, 69)],
65
+    'bugn5': [(237, 248, 251), (178, 226, 226), (102, 194, 164), (44, 162, 95), (0, 109, 44)],
66
+    'bugn6': [(237, 248, 251), (204, 236, 230), (153, 216, 201), (102, 194, 164), (44, 162, 95), (0, 109, 44)],
67
+    'bugn7': [(237, 248, 251), (204, 236, 230), (153, 216, 201), (102, 194, 164), (65, 174, 118), (35, 139, 69), (0, 88, 36)],
68
+    'bugn8': [(247, 252, 253), (229, 245, 249), (204, 236, 230), (153, 216, 201), (102, 194, 164), (65, 174, 118), (35, 139, 69), (0, 88, 36)],
69
+    'bugn9': [(247, 252, 253), (229, 245, 249), (204, 236, 230), (153, 216, 201), (102, 194, 164), (65, 174, 118), (35, 139, 69), (0, 109, 44), (0, 68, 27)],
70
+    'bupu3': [(224, 236, 244), (158, 188, 218), (136, 86, 167)],
71
+    'bupu4': [(237, 248, 251), (179, 205, 227), (140, 150, 198), (136, 65, 157)],
72
+    'bupu5': [(237, 248, 251), (179, 205, 227), (140, 150, 198), (136, 86, 167), (129, 15, 124)],
73
+    'bupu6': [(237, 248, 251), (191, 211, 230), (158, 188, 218), (140, 150, 198), (136, 86, 167), (129, 15, 124)],
74
+    'bupu7': [(237, 248, 251), (191, 211, 230), (158, 188, 218), (140, 150, 198), (140, 107, 177), (136, 65, 157), (110, 1, 107)],
75
+    'bupu8': [(247, 252, 253), (224, 236, 244), (191, 211, 230), (158, 188, 218), (140, 150, 198), (140, 107, 177), (136, 65, 157), (110, 1, 107)],
76
+    'bupu9': [(247, 252, 253), (224, 236, 244), (191, 211, 230), (158, 188, 218), (140, 150, 198), (140, 107, 177), (136, 65, 157), (129, 15, 124), (77, 0, 75)],
77
+    'dark23': [(27, 158, 119), (217, 95, 2), (117, 112, 179)],
78
+    'dark24': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138)],
79
+    'dark25': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30)],
80
+    'dark26': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30), (230, 171, 2)],
81
+    'dark27': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30), (230, 171, 2), (166, 118, 29)],
82
+    'dark28': [(27, 158, 119), (217, 95, 2), (117, 112, 179), (231, 41, 138), (102, 166, 30), (230, 171, 2), (166, 118, 29), (102, 102, 102)],
83
+    'gnbu3': [(224, 243, 219), (168, 221, 181), (67, 162, 202)],
84
+    'gnbu4': [(240, 249, 232), (186, 228, 188), (123, 204, 196), (43, 140, 190)],
85
+    'gnbu5': [(240, 249, 232), (186, 228, 188), (123, 204, 196), (67, 162, 202), (8, 104, 172)],
86
+    'gnbu6': [(240, 249, 232), (204, 235, 197), (168, 221, 181), (123, 204, 196), (67, 162, 202), (8, 104, 172)],
87
+    'gnbu7': [(240, 249, 232), (204, 235, 197), (168, 221, 181), (123, 204, 196), (78, 179, 211), (43, 140, 190), (8, 88, 158)],
88
+    'gnbu8': [(247, 252, 240), (224, 243, 219), (204, 235, 197), (168, 221, 181), (123, 204, 196), (78, 179, 211), (43, 140, 190), (8, 88, 158)],
89
+    'gnbu9': [(247, 252, 240), (224, 243, 219), (204, 235, 197), (168, 221, 181), (123, 204, 196), (78, 179, 211), (43, 140, 190), (8, 104, 172), (8, 64, 129)],
90
+    'greens3': [(229, 245, 224), (161, 217, 155), (49, 163, 84)],
91
+    'greens4': [(237, 248, 233), (186, 228, 179), (116, 196, 118), (35, 139, 69)],
92
+    'greens5': [(237, 248, 233), (186, 228, 179), (116, 196, 118), (49, 163, 84), (0, 109, 44)],
93
+    'greens6': [(237, 248, 233), (199, 233, 192), (161, 217, 155), (116, 196, 118), (49, 163, 84), (0, 109, 44)],
94
+    'greens7': [(237, 248, 233), (199, 233, 192), (161, 217, 155), (116, 196, 118), (65, 171, 93), (35, 139, 69), (0, 90, 50)],
95
+    'greens8': [(247, 252, 245), (229, 245, 224), (199, 233, 192), (161, 217, 155), (116, 196, 118), (65, 171, 93), (35, 139, 69), (0, 90, 50)],
96
+    'greens9': [(247, 252, 245), (229, 245, 224), (199, 233, 192), (161, 217, 155), (116, 196, 118), (65, 171, 93), (35, 139, 69), (0, 109, 44), (0, 68, 27)],
97
+    'greys3': [(240, 240, 240), (189, 189, 189), (99, 99, 99)],
98
+    'greys4': [(247, 247, 247), (204, 204, 204), (150, 150, 150), (82, 82, 82)],
99
+    'greys5': [(247, 247, 247), (204, 204, 204), (150, 150, 150), (99, 99, 99), (37, 37, 37)],
100
+    'greys6': [(247, 247, 247), (217, 217, 217), (189, 189, 189), (150, 150, 150), (99, 99, 99), (37, 37, 37)],
101
+    'greys7': [(247, 247, 247), (217, 217, 217), (189, 189, 189), (150, 150, 150), (115, 115, 115), (82, 82, 82), (37, 37, 37)],
102
+    'greys8': [(255, 255, 255), (240, 240, 240), (217, 217, 217), (189, 189, 189), (150, 150, 150), (115, 115, 115), (82, 82, 82), (37, 37, 37)],
103
+    'greys9': [(255, 255, 255), (240, 240, 240), (217, 217, 217), (189, 189, 189), (150, 150, 150), (115, 115, 115), (82, 82, 82), (37, 37, 37), (0, 0, 0)],
104
+    'oranges3': [(254, 230, 206), (253, 174, 107), (230, 85, 13)],
105
+    'oranges4': [(254, 237, 222), (253, 190, 133), (253, 141, 60), (217, 71, 1)],
106
+    'oranges5': [(254, 237, 222), (253, 190, 133), (253, 141, 60), (230, 85, 13), (166, 54, 3)],
107
+    'oranges6': [(254, 237, 222), (253, 208, 162), (253, 174, 107), (253, 141, 60), (230, 85, 13), (166, 54, 3)],
108
+    'oranges7': [(254, 237, 222), (253, 208, 162), (253, 174, 107), (253, 141, 60), (241, 105, 19), (217, 72, 1), (140, 45, 4)],
109
+    'oranges8': [(255, 245, 235), (254, 230, 206), (253, 208, 162), (253, 174, 107), (253, 141, 60), (241, 105, 19), (217, 72, 1), (140, 45, 4)],
110
+    'oranges9': [(255, 245, 235), (254, 230, 206), (253, 208, 162), (253, 174, 107), (253, 141, 60), (241, 105, 19), (217, 72, 1), (166, 54, 3), (127, 39, 4)],
111
+    'orrd3': [(254, 232, 200), (253, 187, 132), (227, 74, 51)],
112
+    'orrd4': [(254, 240, 217), (253, 204, 138), (252, 141, 89), (215, 48, 31)],
113
+    'orrd5': [(254, 240, 217), (253, 204, 138), (252, 141, 89), (227, 74, 51), (179, 0, 0)],
114
+    'orrd6': [(254, 240, 217), (253, 212, 158), (253, 187, 132), (252, 141, 89), (227, 74, 51), (179, 0, 0)],
115
+    'orrd7': [(254, 240, 217), (253, 212, 158), (253, 187, 132), (252, 141, 89), (239, 101, 72), (215, 48, 31), (153, 0, 0)],
116
+    'orrd8': [(255, 247, 236), (254, 232, 200), (253, 212, 158), (253, 187, 132), (252, 141, 89), (239, 101, 72), (215, 48, 31), (153, 0, 0)],
117
+    'orrd9': [(255, 247, 236), (254, 232, 200), (253, 212, 158), (253, 187, 132), (252, 141, 89), (239, 101, 72), (215, 48, 31), (179, 0, 0), (127, 0, 0)],
118
+    'paired10': [(166, 206, 227), (106, 61, 154), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
119
+    'paired11': [(166, 206, 227), (106, 61, 154), (255, 255, 153), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
120
+    'paired12': [(166, 206, 227), (106, 61, 154), (255, 255, 153), (177, 89, 40), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
121
+    'paired3': [(166, 206, 227), (31, 120, 180), (178, 223, 138)],
122
+    'paired4': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44)],
123
+    'paired5': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153)],
124
+    'paired6': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28)],
125
+    'paired7': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111)],
126
+    'paired8': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0)],
127
+    'paired9': [(166, 206, 227), (31, 120, 180), (178, 223, 138), (51, 160, 44), (251, 154, 153), (227, 26, 28), (253, 191, 111), (255, 127, 0), (202, 178, 214)],
128
+    'pastel13': [(251, 180, 174), (179, 205, 227), (204, 235, 197)],
129
+    'pastel14': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228)],
130
+    'pastel15': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166)],
131
+    'pastel16': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204)],
132
+    'pastel17': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204), (229, 216, 189)],
133
+    'pastel18': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204), (229, 216, 189), (253, 218, 236)],
134
+    'pastel19': [(251, 180, 174), (179, 205, 227), (204, 235, 197), (222, 203, 228), (254, 217, 166), (255, 255, 204), (229, 216, 189), (253, 218, 236), (242, 242, 242)],
135
+    'pastel23': [(179, 226, 205), (253, 205, 172), (203, 213, 232)],
136
+    'pastel24': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228)],
137
+    'pastel25': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201)],
138
+    'pastel26': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201), (255, 242, 174)],
139
+    'pastel27': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201), (255, 242, 174), (241, 226, 204)],
140
+    'pastel28': [(179, 226, 205), (253, 205, 172), (203, 213, 232), (244, 202, 228), (230, 245, 201), (255, 242, 174), (241, 226, 204), (204, 204, 204)],
141
+    'piyg10': [(142, 1, 82), (39, 100, 25), (197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (230, 245, 208), (184, 225, 134), (127, 188, 65), (77, 146, 33)],
142
+    'piyg11': [(142, 1, 82), (77, 146, 33), (39, 100, 25), (197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (247, 247, 247), (230, 245, 208), (184, 225, 134), (127, 188, 65)],
143
+    'piyg3': [(233, 163, 201), (247, 247, 247), (161, 215, 106)],
144
+    'piyg4': [(208, 28, 139), (241, 182, 218), (184, 225, 134), (77, 172, 38)],
145
+    'piyg5': [(208, 28, 139), (241, 182, 218), (247, 247, 247), (184, 225, 134), (77, 172, 38)],
146
+    'piyg6': [(197, 27, 125), (233, 163, 201), (253, 224, 239), (230, 245, 208), (161, 215, 106), (77, 146, 33)],
147
+    'piyg7': [(197, 27, 125), (233, 163, 201), (253, 224, 239), (247, 247, 247), (230, 245, 208), (161, 215, 106), (77, 146, 33)],
148
+    'piyg8': [(197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (230, 245, 208), (184, 225, 134), (127, 188, 65), (77, 146, 33)],
149
+    'piyg9': [(197, 27, 125), (222, 119, 174), (241, 182, 218), (253, 224, 239), (247, 247, 247), (230, 245, 208), (184, 225, 134), (127, 188, 65), (77, 146, 33)],
150
+    'prgn10': [(64, 0, 75), (0, 68, 27), (118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (217, 240, 211), (166, 219, 160), (90, 174, 97), (27, 120, 55)],
151
+    'prgn11': [(64, 0, 75), (27, 120, 55), (0, 68, 27), (118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (247, 247, 247), (217, 240, 211), (166, 219, 160), (90, 174, 97)],
152
+    'prgn3': [(175, 141, 195), (247, 247, 247), (127, 191, 123)],
153
+    'prgn4': [(123, 50, 148), (194, 165, 207), (166, 219, 160), (0, 136, 55)],
154
+    'prgn5': [(123, 50, 148), (194, 165, 207), (247, 247, 247), (166, 219, 160), (0, 136, 55)],
155
+    'prgn6': [(118, 42, 131), (175, 141, 195), (231, 212, 232), (217, 240, 211), (127, 191, 123), (27, 120, 55)],
156
+    'prgn7': [(118, 42, 131), (175, 141, 195), (231, 212, 232), (247, 247, 247), (217, 240, 211), (127, 191, 123), (27, 120, 55)],
157
+    'prgn8': [(118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (217, 240, 211), (166, 219, 160), (90, 174, 97), (27, 120, 55)],
158
+    'prgn9': [(118, 42, 131), (153, 112, 171), (194, 165, 207), (231, 212, 232), (247, 247, 247), (217, 240, 211), (166, 219, 160), (90, 174, 97), (27, 120, 55)],
159
+    'pubu3': [(236, 231, 242), (166, 189, 219), (43, 140, 190)],
160
+    'pubu4': [(241, 238, 246), (189, 201, 225), (116, 169, 207), (5, 112, 176)],
161
+    'pubu5': [(241, 238, 246), (189, 201, 225), (116, 169, 207), (43, 140, 190), (4, 90, 141)],
162
+    'pubu6': [(241, 238, 246), (208, 209, 230), (166, 189, 219), (116, 169, 207), (43, 140, 190), (4, 90, 141)],
163
+    'pubu7': [(241, 238, 246), (208, 209, 230), (166, 189, 219), (116, 169, 207), (54, 144, 192), (5, 112, 176), (3, 78, 123)],
164
+    'pubu8': [(255, 247, 251), (236, 231, 242), (208, 209, 230), (166, 189, 219), (116, 169, 207), (54, 144, 192), (5, 112, 176), (3, 78, 123)],
165
+    'pubu9': [(255, 247, 251), (236, 231, 242), (208, 209, 230), (166, 189, 219), (116, 169, 207), (54, 144, 192), (5, 112, 176), (4, 90, 141), (2, 56, 88)],
166
+    'pubugn3': [(236, 226, 240), (166, 189, 219), (28, 144, 153)],
167
+    'pubugn4': [(246, 239, 247), (189, 201, 225), (103, 169, 207), (2, 129, 138)],
168
+    'pubugn5': [(246, 239, 247), (189, 201, 225), (103, 169, 207), (28, 144, 153), (1, 108, 89)],
169
+    'pubugn6': [(246, 239, 247), (208, 209, 230), (166, 189, 219), (103, 169, 207), (28, 144, 153), (1, 108, 89)],
170
+    'pubugn7': [(246, 239, 247), (208, 209, 230), (166, 189, 219), (103, 169, 207), (54, 144, 192), (2, 129, 138), (1, 100, 80)],
171
+    'pubugn8': [(255, 247, 251), (236, 226, 240), (208, 209, 230), (166, 189, 219), (103, 169, 207), (54, 144, 192), (2, 129, 138), (1, 100, 80)],
172
+    'pubugn9': [(255, 247, 251), (236, 226, 240), (208, 209, 230), (166, 189, 219), (103, 169, 207), (54, 144, 192), (2, 129, 138), (1, 108, 89), (1, 70, 54)],
173
+    'puor10': [(127, 59, 8), (45, 0, 75), (179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (216, 218, 235), (178, 171, 210), (128, 115, 172), (84, 39, 136)],
174
+    'puor11': [(127, 59, 8), (84, 39, 136), (45, 0, 75), (179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (247, 247, 247), (216, 218, 235), (178, 171, 210), (128, 115, 172)],
175
+    'puor3': [(241, 163, 64), (247, 247, 247), (153, 142, 195)],
176
+    'puor4': [(230, 97, 1), (253, 184, 99), (178, 171, 210), (94, 60, 153)],
177
+    'puor5': [(230, 97, 1), (253, 184, 99), (247, 247, 247), (178, 171, 210), (94, 60, 153)],
178
+    'puor6': [(179, 88, 6), (241, 163, 64), (254, 224, 182), (216, 218, 235), (153, 142, 195), (84, 39, 136)],
179
+    'puor7': [(179, 88, 6), (241, 163, 64), (254, 224, 182), (247, 247, 247), (216, 218, 235), (153, 142, 195), (84, 39, 136)],
180
+    'puor8': [(179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (216, 218, 235), (178, 171, 210), (128, 115, 172), (84, 39, 136)],
181
+    'puor9': [(179, 88, 6), (224, 130, 20), (253, 184, 99), (254, 224, 182), (247, 247, 247), (216, 218, 235), (178, 171, 210), (128, 115, 172), (84, 39, 136)],
182
+    'purd3': [(231, 225, 239), (201, 148, 199), (221, 28, 119)],
183
+    'purd4': [(241, 238, 246), (215, 181, 216), (223, 101, 176), (206, 18, 86)],
184
+    'purd5': [(241, 238, 246), (215, 181, 216), (223, 101, 176), (221, 28, 119), (152, 0, 67)],
185
+    'purd6': [(241, 238, 246), (212, 185, 218), (201, 148, 199), (223, 101, 176), (221, 28, 119), (152, 0, 67)],
186
+    'purd7': [(241, 238, 246), (212, 185, 218), (201, 148, 199), (223, 101, 176), (231, 41, 138), (206, 18, 86), (145, 0, 63)],
187
+    'purd8': [(247, 244, 249), (231, 225, 239), (212, 185, 218), (201, 148, 199), (223, 101, 176), (231, 41, 138), (206, 18, 86), (145, 0, 63)],
188
+    'purd9': [(247, 244, 249), (231, 225, 239), (212, 185, 218), (201, 148, 199), (223, 101, 176), (231, 41, 138), (206, 18, 86), (152, 0, 67), (103, 0, 31)],
189
+    'purples3': [(239, 237, 245), (188, 189, 220), (117, 107, 177)],
190
+    'purples4': [(242, 240, 247), (203, 201, 226), (158, 154, 200), (106, 81, 163)],
191
+    'purples5': [(242, 240, 247), (203, 201, 226), (158, 154, 200), (117, 107, 177), (84, 39, 143)],
192
+    'purples6': [(242, 240, 247), (218, 218, 235), (188, 189, 220), (158, 154, 200), (117, 107, 177), (84, 39, 143)],
193
+    'purples7': [(242, 240, 247), (218, 218, 235), (188, 189, 220), (158, 154, 200), (128, 125, 186), (106, 81, 163), (74, 20, 134)],
194
+    'purples8': [(252, 251, 253), (239, 237, 245), (218, 218, 235), (188, 189, 220), (158, 154, 200), (128, 125, 186), (106, 81, 163), (74, 20, 134)],
195
+    'purples9': [(252, 251, 253), (239, 237, 245), (218, 218, 235), (188, 189, 220), (158, 154, 200), (128, 125, 186), (106, 81, 163), (84, 39, 143), (63, 0, 125)],
196
+    'rdbu10': [(103, 0, 31), (5, 48, 97), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (209, 229, 240), (146, 197, 222), (67, 147, 195), (33, 102, 172)],
197
+    'rdbu11': [(103, 0, 31), (33, 102, 172), (5, 48, 97), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (247, 247, 247), (209, 229, 240), (146, 197, 222), (67, 147, 195)],
198
+    'rdbu3': [(239, 138, 98), (247, 247, 247), (103, 169, 207)],
199
+    'rdbu4': [(202, 0, 32), (244, 165, 130), (146, 197, 222), (5, 113, 176)],
200
+    'rdbu5': [(202, 0, 32), (244, 165, 130), (247, 247, 247), (146, 197, 222), (5, 113, 176)],
201
+    'rdbu6': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (209, 229, 240), (103, 169, 207), (33, 102, 172)],
202
+    'rdbu7': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (247, 247, 247), (209, 229, 240), (103, 169, 207), (33, 102, 172)],
203
+    'rdbu8': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (209, 229, 240), (146, 197, 222), (67, 147, 195), (33, 102, 172)],
204
+    'rdbu9': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (247, 247, 247), (209, 229, 240), (146, 197, 222), (67, 147, 195), (33, 102, 172)],
205
+    'rdgy10': [(103, 0, 31), (26, 26, 26), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (224, 224, 224), (186, 186, 186), (135, 135, 135), (77, 77, 77)],
206
+    'rdgy11': [(103, 0, 31), (77, 77, 77), (26, 26, 26), (178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (255, 255, 255), (224, 224, 224), (186, 186, 186), (135, 135, 135)],
207
+    'rdgy3': [(239, 138, 98), (255, 255, 255), (153, 153, 153)],
208
+    'rdgy4': [(202, 0, 32), (244, 165, 130), (186, 186, 186), (64, 64, 64)],
209
+    'rdgy5': [(202, 0, 32), (244, 165, 130), (255, 255, 255), (186, 186, 186), (64, 64, 64)],
210
+    'rdgy6': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (224, 224, 224), (153, 153, 153), (77, 77, 77)],
211
+    'rdgy7': [(178, 24, 43), (239, 138, 98), (253, 219, 199), (255, 255, 255), (224, 224, 224), (153, 153, 153), (77, 77, 77)],
212
+    'rdgy8': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (224, 224, 224), (186, 186, 186), (135, 135, 135), (77, 77, 77)],
213
+    'rdgy9': [(178, 24, 43), (214, 96, 77), (244, 165, 130), (253, 219, 199), (255, 255, 255), (224, 224, 224), (186, 186, 186), (135, 135, 135), (77, 77, 77)],
214
+    'rdpu3': [(253, 224, 221), (250, 159, 181), (197, 27, 138)],
215
+    'rdpu4': [(254, 235, 226), (251, 180, 185), (247, 104, 161), (174, 1, 126)],
216
+    'rdpu5': [(254, 235, 226), (251, 180, 185), (247, 104, 161), (197, 27, 138), (122, 1, 119)],
217
+    'rdpu6': [(254, 235, 226), (252, 197, 192), (250, 159, 181), (247, 104, 161), (197, 27, 138), (122, 1, 119)],
218
+    'rdpu7': [(254, 235, 226), (252, 197, 192), (250, 159, 181), (247, 104, 161), (221, 52, 151), (174, 1, 126), (122, 1, 119)],
219
+    'rdpu8': [(255, 247, 243), (253, 224, 221), (252, 197, 192), (250, 159, 181), (247, 104, 161), (221, 52, 151), (174, 1, 126), (122, 1, 119)],
220
+    'rdpu9': [(255, 247, 243), (253, 224, 221), (252, 197, 192), (250, 159, 181), (247, 104, 161), (221, 52, 151), (174, 1, 126), (122, 1, 119), (73, 0, 106)],
221
+    'rdylbu10': [(165, 0, 38), (49, 54, 149), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (224, 243, 248), (171, 217, 233), (116, 173, 209), (69, 117, 180)],
222
+    'rdylbu11': [(165, 0, 38), (69, 117, 180), (49, 54, 149), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (255, 255, 191), (224, 243, 248), (171, 217, 233), (116, 173, 209)],
223
+    'rdylbu3': [(252, 141, 89), (255, 255, 191), (145, 191, 219)],
224
+    'rdylbu4': [(215, 25, 28), (253, 174, 97), (171, 217, 233), (44, 123, 182)],
225
+    'rdylbu5': [(215, 25, 28), (253, 174, 97), (255, 255, 191), (171, 217, 233), (44, 123, 182)],
226
+    'rdylbu6': [(215, 48, 39), (252, 141, 89), (254, 224, 144), (224, 243, 248), (145, 191, 219), (69, 117, 180)],
227
+    'rdylbu7': [(215, 48, 39), (252, 141, 89), (254, 224, 144), (255, 255, 191), (224, 243, 248), (145, 191, 219), (69, 117, 180)],
228
+    'rdylbu8': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (224, 243, 248), (171, 217, 233), (116, 173, 209), (69, 117, 180)],
229
+    'rdylbu9': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 144), (255, 255, 191), (224, 243, 248), (171, 217, 233), (116, 173, 209), (69, 117, 180)],
230
+    'rdylgn10': [(165, 0, 38), (0, 104, 55), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (217, 239, 139), (166, 217, 106), (102, 189, 99), (26, 152, 80)],
231
+    'rdylgn11': [(165, 0, 38), (26, 152, 80), (0, 104, 55), (215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (217, 239, 139), (166, 217, 106), (102, 189, 99)],
232
+    'rdylgn3': [(252, 141, 89), (255, 255, 191), (145, 207, 96)],
233
+    'rdylgn4': [(215, 25, 28), (253, 174, 97), (166, 217, 106), (26, 150, 65)],
234
+    'rdylgn5': [(215, 25, 28), (253, 174, 97), (255, 255, 191), (166, 217, 106), (26, 150, 65)],
235
+    'rdylgn6': [(215, 48, 39), (252, 141, 89), (254, 224, 139), (217, 239, 139), (145, 207, 96), (26, 152, 80)],
236
+    'rdylgn7': [(215, 48, 39), (252, 141, 89), (254, 224, 139), (255, 255, 191), (217, 239, 139), (145, 207, 96), (26, 152, 80)],
237
+    'rdylgn8': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (217, 239, 139), (166, 217, 106), (102, 189, 99), (26, 152, 80)],
238
+    'rdylgn9': [(215, 48, 39), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (217, 239, 139), (166, 217, 106), (102, 189, 99), (26, 152, 80)],
239
+    'reds3': [(254, 224, 210), (252, 146, 114), (222, 45, 38)],
240
+    'reds4': [(254, 229, 217), (252, 174, 145), (251, 106, 74), (203, 24, 29)],
241
+    'reds5': [(254, 229, 217), (252, 174, 145), (251, 106, 74), (222, 45, 38), (165, 15, 21)],
242
+    'reds6': [(254, 229, 217), (252, 187, 161), (252, 146, 114), (251, 106, 74), (222, 45, 38), (165, 15, 21)],
243
+    'reds7': [(254, 229, 217), (252, 187, 161), (252, 146, 114), (251, 106, 74), (239, 59, 44), (203, 24, 29), (153, 0, 13)],
244
+    'reds8': [(255, 245, 240), (254, 224, 210), (252, 187, 161), (252, 146, 114), (251, 106, 74), (239, 59, 44), (203, 24, 29), (153, 0, 13)],
245
+    'reds9': [(255, 245, 240), (254, 224, 210), (252, 187, 161), (252, 146, 114), (251, 106, 74), (239, 59, 44), (203, 24, 29), (165, 15, 21), (103, 0, 13)],
246
+    'set13': [(228, 26, 28), (55, 126, 184), (77, 175, 74)],
247
+    'set14': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163)],
248
+    'set15': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0)],
249
+    'set16': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51)],
250
+    'set17': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51), (166, 86, 40)],
251
+    'set18': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51), (166, 86, 40), (247, 129, 191)],
252
+    'set19': [(228, 26, 28), (55, 126, 184), (77, 175, 74), (152, 78, 163), (255, 127, 0), (255, 255, 51), (166, 86, 40), (247, 129, 191), (153, 153, 153)],
253
+    'set23': [(102, 194, 165), (252, 141, 98), (141, 160, 203)],
254
+    'set24': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195)],
255
+    'set25': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84)],
256
+    'set26': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84), (255, 217, 47)],
257
+    'set27': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84), (255, 217, 47), (229, 196, 148)],
258
+    'set28': [(102, 194, 165), (252, 141, 98), (141, 160, 203), (231, 138, 195), (166, 216, 84), (255, 217, 47), (229, 196, 148), (179, 179, 179)],
259
+    'set310': [(141, 211, 199), (188, 128, 189), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
260
+    'set311': [(141, 211, 199), (188, 128, 189), (204, 235, 197), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
261
+    'set312': [(141, 211, 199), (188, 128, 189), (204, 235, 197), (255, 237, 111), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
262
+    'set33': [(141, 211, 199), (255, 255, 179), (190, 186, 218)],
263
+    'set34': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114)],
264
+    'set35': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211)],
265
+    'set36': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98)],
266
+    'set37': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105)],
267
+    'set38': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229)],
268
+    'set39': [(141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), (217, 217, 217)],
269
+    'spectral10': [(158, 1, 66), (94, 79, 162), (213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (230, 245, 152), (171, 221, 164), (102, 194, 165), (50, 136, 189)],
270
+    'spectral11': [(158, 1, 66), (50, 136, 189), (94, 79, 162), (213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (230, 245, 152), (171, 221, 164), (102, 194, 165)],
271
+    'spectral3': [(252, 141, 89), (255, 255, 191), (153, 213, 148)],
272
+    'spectral4': [(215, 25, 28), (253, 174, 97), (171, 221, 164), (43, 131, 186)],
273
+    'spectral5': [(215, 25, 28), (253, 174, 97), (255, 255, 191), (171, 221, 164), (43, 131, 186)],
274
+    'spectral6': [(213, 62, 79), (252, 141, 89), (254, 224, 139), (230, 245, 152), (153, 213, 148), (50, 136, 189)],
275
+    'spectral7': [(213, 62, 79), (252, 141, 89), (254, 224, 139), (255, 255, 191), (230, 245, 152), (153, 213, 148), (50, 136, 189)],
276
+    'spectral8': [(213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (230, 245, 152), (171, 221, 164), (102, 194, 165), (50, 136, 189)],
277
+    'spectral9': [(213, 62, 79), (244, 109, 67), (253, 174, 97), (254, 224, 139), (255, 255, 191), (230, 245, 152), (171, 221, 164), (102, 194, 165), (50, 136, 189)],
278
+    'ylgn3': [(247, 252, 185), (173, 221, 142), (49, 163, 84)],
279
+    'ylgn4': [(255, 255, 204), (194, 230, 153), (120, 198, 121), (35, 132, 67)],
280
+    'ylgn5': [(255, 255, 204), (194, 230, 153), (120, 198, 121), (49, 163, 84), (0, 104, 55)],
281
+    'ylgn6': [(255, 255, 204), (217, 240, 163), (173, 221, 142), (120, 198, 121), (49, 163, 84), (0, 104, 55)],
282
+    'ylgn7': [(255, 255, 204), (217, 240, 163), (173, 221, 142), (120, 198, 121), (65, 171, 93), (35, 132, 67), (0, 90, 50)],
283
+    'ylgn8': [(255, 255, 229), (247, 252, 185), (217, 240, 163), (173, 221, 142), (120, 198, 121), (65, 171, 93), (35, 132, 67), (0, 90, 50)],
284
+    'ylgn9': [(255, 255, 229), (247, 252, 185), (217, 240, 163), (173, 221, 142), (120, 198, 121), (65, 171, 93), (35, 132, 67), (0, 104, 55), (0, 69, 41)],
285
+    'ylgnbu3': [(237, 248, 177), (127, 205, 187), (44, 127, 184)],
286
+    'ylgnbu4': [(255, 255, 204), (161, 218, 180), (65, 182, 196), (34, 94, 168)],
287
+    'ylgnbu5': [(255, 255, 204), (161, 218, 180), (65, 182, 196), (44, 127, 184), (37, 52, 148)],
288
+    'ylgnbu6': [(255, 255, 204), (199, 233, 180), (127, 205, 187), (65, 182, 196), (44, 127, 184), (37, 52, 148)],
289
+    'ylgnbu7': [(255, 255, 204), (199, 233, 180), (127, 205, 187), (65, 182, 196), (29, 145, 192), (34, 94, 168), (12, 44, 132)],
290
+    'ylgnbu8': [(255, 255, 217), (237, 248, 177), (199, 233, 180), (127, 205, 187), (65, 182, 196), (29, 145, 192), (34, 94, 168), (12, 44, 132)],
291
+    'ylgnbu9': [(255, 255, 217), (237, 248, 177), (199, 233, 180), (127, 205, 187), (65, 182, 196), (29, 145, 192), (34, 94, 168), (37, 52, 148), (8, 29, 88)],
292
+    'ylorbr3': [(255, 247, 188), (254, 196, 79), (217, 95, 14)],
293
+    'ylorbr4': [(255, 255, 212), (254, 217, 142), (254, 153, 41), (204, 76, 2)],
294
+    'ylorbr5': [(255, 255, 212), (254, 217, 142), (254, 153, 41), (217, 95, 14), (153, 52, 4)],
295
+    'ylorbr6': [(255, 255, 212), (254, 227, 145), (254, 196, 79), (254, 153, 41), (217, 95, 14), (153, 52, 4)],
296
+    'ylorbr7': [(255, 255, 212), (254, 227, 145), (254, 196, 79), (254, 153, 41), (236, 112, 20), (204, 76, 2), (140, 45, 4)],
297
+    'ylorbr8': [(255, 255, 229), (255, 247, 188), (254, 227, 145), (254, 196, 79), (254, 153, 41), (236, 112, 20), (204, 76, 2), (140, 45, 4)],
298
+    'ylorbr9': [(255, 255, 229), (255, 247, 188), (254, 227, 145), (254, 196, 79), (254, 153, 41), (236, 112, 20), (204, 76, 2), (153, 52, 4), (102, 37, 6)],
299
+    'ylorrd3': [(255, 237, 160), (254, 178, 76), (240, 59, 32)],
300
+    'ylorrd4': [(255, 255, 178), (254, 204, 92), (253, 141, 60), (227, 26, 28)],
301
+    'ylorrd5': [(255, 255, 178), (254, 204, 92), (253, 141, 60), (240, 59, 32), (189, 0, 38)],
302
+    'ylorrd6': [(255, 255, 178), (254, 217, 118), (254, 178, 76), (253, 141, 60), (240, 59, 32), (189, 0, 38)],
303
+    'ylorrd7': [(255, 255, 178), (254, 217, 118), (254, 178, 76), (253, 141, 60), (252, 78, 42), (227, 26, 28), (177, 0, 38)],
304
+    'ylorrd8': [(255, 255, 204), (255, 237, 160), (254, 217, 118), (254, 178, 76), (253, 141, 60), (252, 78, 42), (227, 26, 28), (177, 0, 38)],
305
+}
0 306
new file mode 100644
... ...
@@ -0,0 +1,486 @@
1
+# Copyright 2008-2015 Jose Fonseca
2
+#
3
+# This program is free software: you can redistribute it and/or modify it
4
+# under the terms of the GNU Lesser General Public License as published
5
+# by the Free Software Foundation, either version 3 of the License, or
6
+# (at your option) any later version.
7
+#
8
+# This program is distributed in the hope that it will be useful,
9
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
+# GNU Lesser General Public License for more details.
12
+#
13
+# You should have received a copy of the GNU Lesser General Public License
14
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
+#
16
+import math
17
+
18
+import gi
19
+gi.require_version('Gtk', '3.0')
20
+gi.require_version('PangoCairo', '1.0')
21
+
22
+from gi.repository import GObject
23
+from gi.repository import Gdk
24
+from gi.repository import GdkPixbuf
25
+from gi.repository import Pango
26
+from gi.repository import PangoCairo
27
+import cairo
28
+
29
+
30
+class Shape:
31
+    """Abstract base class for all the drawing shapes."""
32
+
33
+    def __init__(self):
34
+        pass
35
+
36
+    def draw(self, cr, highlight=False):
37
+        """Draw this shape with the given cairo context"""
38
+        raise NotImplementedError
39
+
40
+    def select_pen(self, highlight):
41
+        if highlight:
42
+            if not hasattr(self, 'highlight_pen'):
43
+                self.highlight_pen = self.pen.highlighted()
44
+            return self.highlight_pen
45
+        else:
46
+            return self.pen
47
+
48
+    def search_text(self, regexp):
49
+        return False
50
+
51
+
52
+class TextShape(Shape):
53
+
54
+    LEFT, CENTER, RIGHT = -1, 0, 1
55
+
56
+    def __init__(self, pen, x, y, j, w, t):
57
+        Shape.__init__(self)
58
+        self.pen = pen.copy()
59
+        self.x = x
60
+        self.y = y
61
+        self.j = j  # Centering
62
+        self.w = w  # width
63
+        self.t = t  # text
64
+
65
+    def draw(self, cr, highlight=False):
66
+
67
+        try:
68
+            layout = self.layout
69
+        except AttributeError:
70
+            layout = PangoCairo.create_layout(cr)
71
+
72
+            # set font options
73
+            # see http://lists.freedesktop.org/archives/cairo/2007-February/009688.html
74
+            context = layout.get_context()
75
+            fo = cairo.FontOptions()
76
+            fo.set_antialias(cairo.ANTIALIAS_DEFAULT)
77
+            fo.set_hint_style(cairo.HINT_STYLE_NONE)
78
+            fo.set_hint_metrics(cairo.HINT_METRICS_OFF)
79
+            try:
80
+                PangoCairo.context_set_font_options(context, fo)
81
+            except TypeError:
82
+                # XXX: Some broken pangocairo bindings show the error
83
+                # 'TypeError: font_options must be a cairo.FontOptions or None'
84
+                pass
85
+            except KeyError:
86
+                # cairo.FontOptions is not registered as a foreign struct in older PyGObject versions.
87
+                # https://git.gnome.org/browse/pygobject/commit/?id=b21f66d2a399b8c9a36a1758107b7bdff0ec8eaa
88
+                pass
89
+
90
+            # set font
91
+            font = Pango.FontDescription()
92
+
93
+            # https://developer.gnome.org/pango/stable/PangoMarkupFormat.html
94
+            markup = GObject.markup_escape_text(self.t)
95
+            if self.pen.bold:
96
+                markup = '<b>' + markup + '</b>'
97
+            if self.pen.italic:
98
+                markup = '<i>' + markup + '</i>'
99
+            if self.pen.underline:
100
+                markup = '<span underline="single">' + markup + '</span>'
101
+            if self.pen.strikethrough:
102
+                markup = '<s>' + markup + '</s>'
103
+            if self.pen.superscript:
104
+                markup = '<sup><small>' + markup + '</small></sup>'
105
+            if self.pen.subscript:
106
+                markup = '<sub><small>' + markup + '</small></sub>'
107
+
108
+            success, attrs, text, accel_char = Pango.parse_markup(markup, -1, '\x00')
109
+            assert success
110
+            layout.set_attributes(attrs)
111
+
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(text, -1)
118
+
119
+            # cache it
120
+            self.layout = layout
121
+        else:
122
+            PangoCairo.update_layout(cr, layout)
123
+
124
+        descent = 2 # XXX get descender from font metrics
125
+
126
+        width, height = layout.get_size()
127
+        width = float(width)/Pango.SCALE
128
+        height = float(height)/Pango.SCALE
129
+
130
+        # we know the width that dot thinks this text should have
131
+        # we do not necessarily have a font with the same metrics
132
+        # scale it so that the text fits inside its box
133
+        if width > self.w:
134
+            f = self.w / width
135
+            width = self.w # equivalent to width *= f
136
+            height *= f
137
+            descent *= f
138
+        else:
139
+            f = 1.0
140
+
141
+        if self.j == self.LEFT:
142
+            x = self.x
143
+        elif self.j == self.CENTER:
144
+            x = self.x - 0.5*width
145
+        elif self.j == self.RIGHT:
146
+            x = self.x - width
147
+        else:
148
+            assert 0
149
+
150
+        y = self.y - height + descent
151
+
152
+        cr.move_to(x, y)
153
+
154
+        cr.save()
155
+        cr.scale(f, f)
156
+        cr.set_source_rgba(*self.select_pen(highlight).color)
157
+        PangoCairo.show_layout(cr, layout)
158
+        cr.restore()
159
+
160
+        if 0: # DEBUG
161
+            # show where dot thinks the text should appear
162
+            cr.set_source_rgba(1, 0, 0, .9)
163
+            if self.j == self.LEFT:
164
+                x = self.x
165
+            elif self.j == self.CENTER:
166
+                x = self.x - 0.5*self.w
167
+            elif self.j == self.RIGHT:
168
+                x = self.x - self.w
169
+            cr.move_to(x, self.y)
170
+            cr.line_to(x+self.w, self.y)
171
+            cr.stroke()
172
+
173
+    def search_text(self, regexp):
174
+        return regexp.search(self.t) is not None
175
+
176
+
177
+class ImageShape(Shape):
178
+
179
+    def __init__(self, pen, x0, y0, w, h, path):
180
+        Shape.__init__(self)
181
+        self.pen = pen.copy()
182
+        self.x0 = x0
183
+        self.y0 = y0
184
+        self.w = w
185
+        self.h = h
186
+        self.path = path
187
+
188
+    def draw(self, cr, highlight=False):
189
+        pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.path)
190
+        sx = float(self.w)/float(pixbuf.get_width())
191
+        sy = float(self.h)/float(pixbuf.get_height())
192
+        cr.save()
193
+        cr.translate(self.x0, self.y0 - self.h)
194
+        cr.scale(sx, sy)
195
+        Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0)
196
+        cr.paint()
197
+        cr.restore()
198
+
199
+
200
+class EllipseShape(Shape):
201
+
202
+    def __init__(self, pen, x0, y0, w, h, filled=False):
203
+        Shape.__init__(self)
204
+        self.pen = pen.copy()
205
+        self.x0 = x0
206
+        self.y0 = y0
207
+        self.w = w
208
+        self.h = h
209
+        self.filled = filled
210
+
211
+    def draw(self, cr, highlight=False):
212
+        cr.save()
213
+        cr.translate(self.x0, self.y0)
214
+        cr.scale(self.w, self.h)
215
+        cr.move_to(1.0, 0.0)
216
+        cr.arc(0.0, 0.0, 1.0, 0, 2.0*math.pi)
217
+        cr.restore()
218
+        pen = self.select_pen(highlight)
219
+        if self.filled:
220
+            cr.set_source_rgba(*pen.fillcolor)
221
+            cr.fill()
222
+        else:
223
+            cr.set_dash(pen.dash)
224
+            cr.set_line_width(pen.linewidth)
225
+            cr.set_source_rgba(*pen.color)
226
+            cr.stroke()
227
+
228
+
229
+class PolygonShape(Shape):
230
+
231
+    def __init__(self, pen, points, filled=False):
232
+        Shape.__init__(self)
233
+        self.pen = pen.copy()
234
+        self.points = points
235
+        self.filled = filled
236
+
237
+    def draw(self, cr, highlight=False):
238
+        x0, y0 = self.points[-1]
239
+        cr.move_to(x0, y0)
240
+        for x, y in self.points:
241
+            cr.line_to(x, y)
242
+        cr.close_path()
243
+        pen = self.select_pen(highlight)
244
+        if self.filled:
245
+            cr.set_source_rgba(*pen.fillcolor)
246
+            cr.fill_preserve()
247
+            cr.fill()
248
+        else:
249
+            cr.set_dash(pen.dash)
250
+            cr.set_line_width(pen.linewidth)
251
+            cr.set_source_rgba(*pen.color)
252
+            cr.stroke()
253
+
254
+
255
+class LineShape(Shape):
256
+
257
+    def __init__(self, pen, points):
258
+        Shape.__init__(self)
259
+        self.pen = pen.copy()
260
+        self.points = points
261
+
262
+    def draw(self, cr, highlight=False):
263
+        x0, y0 = self.points[0]
264
+        cr.move_to(x0, y0)
265
+        for x1, y1 in self.points[1:]:
266
+            cr.line_to(x1, y1)
267
+        pen = self.select_pen(highlight)
268
+        cr.set_dash(pen.dash)
269
+        cr.set_line_width(pen.linewidth)
270
+        cr.set_source_rgba(*pen.color)
271
+        cr.stroke()
272
+
273
+
274
+class BezierShape(Shape):
275
+
276
+    def __init__(self, pen, points, filled=False):
277
+        Shape.__init__(self)
278
+        self.pen = pen.copy()
279
+        self.points = points
280
+        self.filled = filled
281
+
282
+    def draw(self, cr, highlight=False):
283
+        x0, y0 = self.points[0]
284
+        cr.move_to(x0, y0)
285
+        for i in range(1, len(self.points), 3):
286
+            x1, y1 = self.points[i]
287
+            x2, y2 = self.points[i + 1]
288
+            x3, y3 = self.points[i + 2]
289
+            cr.curve_to(x1, y1, x2, y2, x3, y3)
290
+        pen = self.select_pen(highlight)
291
+        if self.filled:
292
+            cr.set_source_rgba(*pen.fillcolor)
293
+            cr.fill_preserve()
294
+            cr.fill()
295
+        else:
296
+            cr.set_dash(pen.dash)
297
+            cr.set_line_width(pen.linewidth)
298
+            cr.set_source_rgba(*pen.color)
299
+            cr.stroke()
300
+
301
+
302
+class CompoundShape(Shape):
303
+
304
+    def __init__(self, shapes):
305
+        Shape.__init__(self)
306
+        self.shapes = shapes
307
+
308
+    def draw(self, cr, highlight=False):
309
+        for shape in self.shapes:
310
+            shape.draw(cr, highlight=highlight)
311
+
312
+    def search_text(self, regexp):
313
+        for shape in self.shapes:
314
+            if shape.search_text(regexp):
315
+                return True
316
+        return False
317
+
318
+
319
+class Url(object):
320
+
321
+    def __init__(self, item, url, highlight=None):
322
+        self.item = item
323
+        self.url = url
324
+        if highlight is None:
325
+            highlight = set([item])
326
+        self.highlight = highlight
327
+
328
+
329
+class Jump(object):
330
+
331
+    def __init__(self, item, x, y, highlight=None):
332
+        self.item = item
333
+        self.x = x
334
+        self.y = y
335
+        if highlight is None:
336
+            highlight = set([item])
337
+        self.highlight = highlight
338
+
339
+
340
+class Element(CompoundShape):
341
+    """Base class for graph nodes and edges."""
342
+
343
+    def __init__(self, shapes):
344
+        CompoundShape.__init__(self, shapes)
345
+
346
+    def is_inside(self, x, y):
347
+        return False
348
+
349
+    def get_url(self, x, y):
350
+        return None
351
+
352
+    def get_jump(self, x, y):
353
+        return None
354
+
355
+
356
+class Node(Element):
357
+
358
+    def __init__(self, id, x, y, w, h, shapes, url):
359
+        Element.__init__(self, shapes)
360
+
361
+        self.id = id
362
+        self.x = x
363
+        self.y = y
364
+
365
+        self.x1 = x - 0.5*w
366
+        self.y1 = y - 0.5*h
367
+        self.x2 = x + 0.5*w
368
+        self.y2 = y + 0.5*h
369
+
370
+        self.url = url
371
+
372
+    def is_inside(self, x, y):
373
+        return self.x1 <= x and x <= self.x2 and self.y1 <= y and y <= self.y2
374
+
375
+    def get_url(self, x, y):
376
+        if self.url is None:
377
+            return None
378
+        if self.is_inside(x, y):
379
+            return Url(self, self.url)
380
+        return None
381
+
382
+    def get_jump(self, x, y):
383
+        if self.is_inside(x, y):
384
+            return Jump(self, self.x, self.y)
385
+        return None
386
+
387
+    def __repr__(self):
388
+        return "<Node %s>" % self.id
389
+
390
+
391
+def square_distance(x1, y1, x2, y2):
392
+    deltax = x2 - x1
393
+    deltay = y2 - y1
394
+    return deltax*deltax + deltay*deltay
395
+
396
+
397
+class Edge(Element):
398
+
399
+    def __init__(self, src, dst, points, shapes):
400
+        Element.__init__(self, shapes)
401
+        self.src = src
402
+        self.dst = dst
403
+        self.points = points
404
+
405
+    RADIUS = 10
406
+
407
+    def is_inside_begin(self, x, y):
408
+        return square_distance(x, y, *self.points[0]) <= self.RADIUS*self.RADIUS
409
+
410
+    def is_inside_end(self, x, y):
411
+        return square_distance(x, y, *self.points[-1]) <= self.RADIUS*self.RADIUS
412
+
413
+    def is_inside(self, x, y):
414
+        if self.is_inside_begin(x, y):
415
+            return True
416
+        if self.is_inside_end(x, y):
417
+            return True
418
+        return False
419
+
420
+    def get_jump(self, x, y):
421
+        if self.is_inside_begin(x, y):
422
+            return Jump(self, self.dst.x, self.dst.y, highlight=set([self, self.dst]))
423
+        if self.is_inside_end(x, y):
424
+            return Jump(self, self.src.x, self.src.y, highlight=set([self, self.src]))
425
+        return None
426
+
427
+    def __repr__(self):
428
+        return "<Edge %s -> %s>" % (self.src, self.dst)
429
+
430
+
431
+class Graph(Shape):
432
+
433
+    def __init__(self, width=1, height=1, shapes=(), nodes=(), edges=()):
434
+        Shape.__init__(self)
435
+
436
+        self.width = width
437
+        self.height = height
438
+        self.shapes = shapes
439
+        self.nodes = nodes
440
+        self.edges = edges
441
+
442
+    def get_size(self):
443
+        return self.width, self.height
444
+
445
+    def draw(self, cr, highlight_items=None):
446
+        if highlight_items is None:
447
+            highlight_items = ()
448
+        cr.set_source_rgba(0.0, 0.0, 0.0, 1.0)
449
+
450
+        cr.set_line_cap(cairo.LINE_CAP_BUTT)
451
+        cr.set_line_join(cairo.LINE_JOIN_MITER)
452
+
453
+        for shape in self.shapes:
454
+            shape.draw(cr)
455
+        for edge in self.edges:
456
+            should_highlight = any(e in highlight_items
457
+                                   for e in (edge, edge.src, edge.dst))
458
+            edge.draw(cr, highlight=should_highlight)
459
+        for node in self.nodes:
460
+            node.draw(cr, highlight=(node in highlight_items))
461
+
462
+    def get_element(self, x, y):
463
+        for node in self.nodes:
464
+            if node.is_inside(x, y):
465
+                return node
466
+        for edge in self.edges:
467
+            if edge.is_inside(x, y):
468
+                return edge
469
+
470
+    def get_url(self, x, y):
471
+        for node in self.nodes:
472
+            url = node.get_url(x, y)
473
+            if url is not None:
474
+                return url
475
+        return None
476
+
477
+    def get_jump(self, x, y):
478
+        for edge in self.edges:
479
+            jump = edge.get_jump(x, y)
480
+            if jump is not None:
481
+                return jump
482
+        for node in self.nodes:
483
+            jump = node.get_jump(x, y)
484
+            if jump is not None:
485
+                return jump
486
+        return None
0 487
new file mode 100644
... ...
@@ -0,0 +1,54 @@
1
+# Copyright 2008-2015 Jose Fonseca
2
+#
3
+# This program is free software: you can redistribute it and/or modify it
4
+# under the terms of the GNU Lesser General Public License as published
5
+# by the Free Software Foundation, either version 3 of the License, or
6
+# (at your option) any later version.
7
+#
8
+# This program is distributed in the hope that it will be useful,
9
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
+# GNU Lesser General Public License for more details.
12
+#
13
+# You should have received a copy of the GNU Lesser General Public License
14
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
+#
16
+BOLD = 1
17
+ITALIC = 2
18
+UNDERLINE = 4
19
+SUPERSCRIPT = 8
20
+SUBSCRIPT = 16
21
+STRIKE_THROUGH = 32
22
+OVERLINE = 64
23
+
24
+class Pen:
25
+    """Store pen attributes."""
26
+
27
+    def __init__(self):
28
+        # set default attributes
29
+        self.color = (0.0, 0.0, 0.0, 1.0)
30
+        self.fillcolor = (0.0, 0.0, 0.0, 1.0)
31
+        self.linewidth = 1.0
32
+        self.fontsize = 14.0
33
+        self.fontname = "Times-Roman"
34
+        self.bold = False
35
+        self.italic = False
36
+        self.underline = False
37
+        self.superscript = False
38
+        self.subscript = False
39
+        self.strikethrough = False
40
+        self.overline = False
41
+
42
+        self.dash = ()
43
+
44
+    def copy(self):
45
+        """Create a copy of this pen."""
46
+        pen = Pen()
47
+        pen.__dict__ = self.__dict__.copy()
48
+        return pen
49
+
50
+    def highlighted(self):
51
+        pen = self.copy()
52
+        pen.color = (1, 0, 0, 1)
53
+        pen.fillcolor = (1, .8, .8, 1)
54
+        return pen
0 55
new file mode 100644
... ...
@@ -0,0 +1,649 @@
1
+# Copyright 2008-2015 Jose Fonseca
2
+#
3
+# This program is free software: you can redistribute it and/or modify it
4
+# under the terms of the GNU Lesser General Public License as published
5
+# by the Free Software Foundation, either version 3 of the License, or
6
+# (at your option) any later version.
7
+#
8
+# This program is distributed in the hope that it will be useful,
9
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
+# GNU Lesser General Public License for more details.
12
+#
13
+# You should have received a copy of the GNU Lesser General Public License
14
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
+#
16
+
17
+import math
18
+import os
19
+import re
20
+import subprocess
21
+import sys
22
+import time
23
+
24
+import gi
25
+gi.require_version('Gtk', '3.0')
26
+gi.require_version('PangoCairo', '1.0')
27
+
28
+from gi.repository import GLib
29
+from gi.repository import GObject
30
+from gi.repository import Gtk
31
+from gi.repository import Gdk
32
+
33
+# See http://www.graphviz.org/pub/scm/graphviz-cairo/plugin/cairo/gvrender_cairo.c
34
+
35
+# For pygtk inspiration and guidance see:
36
+# - http://mirageiv.berlios.de/
37
+# - http://comix.sourceforge.net/
38
+
39
+from . import actions
40
+from ..dot.lexer import ParseError
41
+from ..dot.parser import XDotParser
42
+from .animation import NoAnimation, ZoomToAnimation
43
+from .actions import NullAction, PanAction, ZoomAction, ZoomAreaAction
44
+from .elements import Graph
45
+
46
+
47
+class DotWidget(Gtk.DrawingArea):
48
+    """GTK widget that draws dot graphs."""
49
+
50
+    #TODO GTK3: Second argument has to be of type Gdk.EventButton instead of object.
51
+    __gsignals__ = {
52
+        'clicked' : (GObject.SIGNAL_RUN_LAST, None, (str, object)),
53
+        'error' : (GObject.SIGNAL_RUN_LAST, None, (str,))
54
+    }
55
+
56
+    filter = 'dot'
57
+
58
+    def __init__(self):
59
+        Gtk.DrawingArea.__init__(self)
60
+
61
+        self.graph = Graph()
62
+        self.openfilename = None
63
+
64
+        self.set_can_focus(True)
65
+
66
+        self.connect("draw", self.on_draw)
67
+        self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK)
68
+        self.connect("button-press-event", self.on_area_button_press)
69
+        self.connect("button-release-event", self.on_area_button_release)
70
+        self.add_events(Gdk.EventMask.POINTER_MOTION_MASK |
71
+                        Gdk.EventMask.POINTER_MOTION_HINT_MASK |
72
+                        Gdk.EventMask.BUTTON_RELEASE_MASK |
73
+                        Gdk.EventMask.SCROLL_MASK)
74
+        self.connect("motion-notify-event", self.on_area_motion_notify)
75
+        self.connect("scroll-event", self.on_area_scroll_event)
76
+        self.connect("size-allocate", self.on_area_size_allocate)
77
+
78
+        self.connect('key-press-event', self.on_key_press_event)
79
+        self.last_mtime = None
80
+
81
+        GLib.timeout_add(1000, self.update)
82
+
83
+        self.x, self.y = 0.0, 0.0
84
+        self.zoom_ratio = 1.0
85
+        self.zoom_to_fit_on_resize = False
86
+        self.animation = NoAnimation(self)
87
+        self.drag_action = NullAction(self)
88
+        self.presstime = None
89
+        self.highlight = None
90
+        self.highlight_search = False
91
+
92
+    def error_dialog(self, message):
93
+        self.emit('error', message)
94
+
95
+    def set_filter(self, filter):
96
+        self.filter = filter
97
+
98
+    def run_filter(self, dotcode):
99
+        if not self.filter:
100
+            return dotcode
101
+        try:
102
+            p = subprocess.Popen(
103
+                [self.filter, '-Txdot'],
104
+                stdin=subprocess.PIPE,
105
+                stdout=subprocess.PIPE,
106
+                stderr=subprocess.PIPE,
107
+                shell=False,
108
+                universal_newlines=False
109
+            )
110
+        except OSError as exc:
111
+            error = '%s: %s' % (self.filter, exc.strerror)
112
+            p = subprocess.CalledProcessError(exc.errno, self.filter, exc.strerror)
113
+        else:
114
+            xdotcode, error = p.communicate(dotcode)
115
+        error = error.rstrip()
116
+        if error:
117
+            error = error.decode()
118
+            sys.stderr.write(error + '\n')
119
+        if p.returncode != 0:
120
+            self.error_dialog(error)
121
+            return None
122
+        return xdotcode
123
+
124
+    def set_dotcode(self, dotcode, filename=None):
125
+        self.openfilename = None
126
+        if isinstance(dotcode, str):
127
+            dotcode = dotcode.encode('utf-8')
128
+        xdotcode = self.run_filter(dotcode)
129
+        if xdotcode is None:
130
+            return False
131
+        try:
132
+            self.set_xdotcode(xdotcode)
133
+        except ParseError as ex:
134
+            self.error_dialog(str(ex))
135
+            return False
136
+        else:
137
+            if filename is None:
138
+                self.last_mtime = None
139
+            else:
140
+                self.last_mtime = os.stat(filename).st_mtime
141
+            self.openfilename = filename
142
+            return True
143
+
144
+    def set_xdotcode(self, xdotcode):
145
+        assert isinstance(xdotcode, bytes)
146
+        parser = XDotParser(xdotcode)
147
+        self.graph = parser.parse()
148
+        self.zoom_image(self.zoom_ratio, center=True)
149
+
150
+    def reload(self):
151
+        if self.openfilename is not None:
152
+            try:
153
+                fp = open(self.openfilename, 'rt')
154
+                self.set_dotcode(fp.read(), self.openfilename)
155
+                fp.close()
156
+            except IOError:
157
+                pass
158
+
159
+    def update(self):
160
+        if self.openfilename is not None:
161
+            current_mtime = os.stat(self.openfilename).st_mtime
162
+            if current_mtime != self.last_mtime:
163
+                self.last_mtime = current_mtime
164
+                self.reload()
165
+        return True
166
+
167
+    def on_draw(self, widget, cr):
168
+        cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
169
+        cr.paint()
170
+
171
+        cr.save()
172
+        rect = self.get_allocation()
173
+        cr.translate(0.5*rect.width, 0.5*rect.height)
174
+        cr.scale(self.zoom_ratio, self.zoom_ratio)
175
+        cr.translate(-self.x, -self.y)
176
+
177
+        self.graph.draw(cr, highlight_items=self.highlight)
178
+        cr.restore()
179
+
180
+        self.drag_action.draw(cr)
181
+
182
+        return False
183
+
184
+    def get_current_pos(self):
185
+        return self.x, self.y
186
+
187
+    def set_current_pos(self, x, y):
188
+        self.x = x
189
+        self.y = y
190
+        self.queue_draw()
191
+
192
+    def set_highlight(self, items, search=False):
193
+        # Enable or disable search highlight
194
+        if search:
195
+            self.highlight_search = items is not None
196
+        # Ignore cursor highlight while searching
197
+        if self.highlight_search and not search:
198
+            return
199
+        if self.highlight != items:
200
+            self.highlight = items
201
+            self.queue_draw()
202
+
203
+    def zoom_image(self, zoom_ratio, center=False, pos=None):
204
+        # Constrain zoom ratio to a sane range to prevent numeric instability.
205
+        zoom_ratio = min(zoom_ratio, 1E4)
206
+        zoom_ratio = max(zoom_ratio, 1E-6)
207
+
208
+        if center:
209
+            self.x = self.graph.width/2
210
+            self.y = self.graph.height/2
211
+        elif pos is not None:
212
+            rect = self.get_allocation()
213
+            x, y = pos
214
+            x -= 0.5*rect.width
215
+            y -= 0.5*rect.height
216
+            self.x += x / self.zoom_ratio - x / zoom_ratio
217
+            self.y += y / self.zoom_ratio - y / zoom_ratio
218
+        self.zoom_ratio = zoom_ratio
219
+        self.zoom_to_fit_on_resize = False
220
+        self.queue_draw()
221
+
222
+    def zoom_to_area(self, x1, y1, x2, y2):
223
+        rect = self.get_allocation()
224
+        width = abs(x1 - x2)
225
+        height = abs(y1 - y2)
226
+        if width == 0 and height == 0:
227
+            self.zoom_ratio *= self.ZOOM_INCREMENT
228
+        else:
229
+            self.zoom_ratio = min(
230
+                float(rect.width)/float(width),
231
+                float(rect.height)/float(height)
232
+            )
233
+        self.zoom_to_fit_on_resize = False
234
+        self.x = (x1 + x2) / 2
235
+        self.y = (y1 + y2) / 2
236
+        self.queue_draw()
237
+
238
+    def zoom_to_fit(self):
239
+        rect = self.get_allocation()
240
+        rect.x += self.ZOOM_TO_FIT_MARGIN
241
+        rect.y += self.ZOOM_TO_FIT_MARGIN
242
+        rect.width -= 2 * self.ZOOM_TO_FIT_MARGIN
243
+        rect.height -= 2 * self.ZOOM_TO_FIT_MARGIN
244
+        zoom_ratio = min(
245
+            float(rect.width)/float(self.graph.width),
246
+            float(rect.height)/float(self.graph.height)
247
+        )
248
+        self.zoom_image(zoom_ratio, center=True)
249
+        self.zoom_to_fit_on_resize = True
250
+
251
+    ZOOM_INCREMENT = 1.25
252
+    ZOOM_TO_FIT_MARGIN = 12
253
+
254
+    def on_zoom_in(self, action):
255
+        self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
256
+
257
+    def on_zoom_out(self, action):
258
+        self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
259
+
260
+    def on_zoom_fit(self, action):
261
+        self.zoom_to_fit()
262
+
263
+    def on_zoom_100(self, action):
264
+        self.zoom_image(1.0)
265
+
266
+    POS_INCREMENT = 100
267
+
268
+    def on_key_press_event(self, widget, event):
269
+        if event.keyval == Gdk.KEY_Left:
270
+            self.x -= self.POS_INCREMENT/self.zoom_ratio
271
+            self.queue_draw()
272
+            return True
273
+        if event.keyval == Gdk.KEY_Right:
274
+            self.x += self.POS_INCREMENT/self.zoom_ratio
275
+            self.queue_draw()
276
+            return True
277
+        if event.keyval == Gdk.KEY_Up:
278
+            self.y -= self.POS_INCREMENT/self.zoom_ratio
279
+            self.queue_draw()
280
+            return True
281
+        if event.keyval == Gdk.KEY_Down:
282
+            self.y += self.POS_INCREMENT/self.zoom_ratio
283
+            self.queue_draw()
284
+            return True
285
+        if event.keyval in (Gdk.KEY_Page_Up,
286
+                            Gdk.KEY_plus,
287
+                            Gdk.KEY_equal,
288
+                            Gdk.KEY_KP_Add):
289
+            self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT)
290
+            self.queue_draw()
291
+            return True
292
+        if event.keyval in (Gdk.KEY_Page_Down,
293
+                            Gdk.KEY_minus,
294
+                            Gdk.KEY_KP_Subtract):
295
+            self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT)
296
+            self.queue_draw()
297
+            return True
298
+        if event.keyval == Gdk.KEY_Escape:
299
+            self.drag_action.abort()
300
+            self.drag_action = NullAction(self)
301
+            return True
302
+        if event.keyval == Gdk.KEY_r:
303
+            self.reload()
304
+            return True
305
+        if event.keyval == Gdk.KEY_f:
306
+            win = widget.get_toplevel()
307
+            find_toolitem = win.uimanager.get_widget('/ToolBar/Find')
308
+            textentry = find_toolitem.get_children()
309
+            win.set_focus(textentry[0])
310
+            return True
311
+        if event.keyval == Gdk.KEY_q:
312
+            Gtk.main_quit()
313
+            return True
314
+        if event.keyval == Gdk.KEY_p:
315
+            self.on_print()
316
+            return True
317
+        return False
318
+
319
+    print_settings = None
320
+    def on_print(self, action=None):
321
+        print_op = Gtk.PrintOperation()
322
+
323
+        if self.print_settings != None:
324
+            print_op.set_print_settings(self.print_settings)
325
+
326
+        print_op.connect("begin_print", self.begin_print)
327
+        print_op.connect("draw_page", self.draw_page)
328
+
329
+        res = print_op.run(Gtk.PrintOperationAction.PRINT_DIALOG, self.get_toplevel())
330
+        if res == Gtk.PrintOperationResult.APPLY:
331
+            self.print_settings = print_op.get_print_settings()
332
+
333
+    def begin_print(self, operation, context):
334
+        operation.set_n_pages(1)
335
+        return True
336
+
337
+    def draw_page(self, operation, context, page_nr):
338
+        cr = context.get_cairo_context()
339
+
340
+        rect = self.get_allocation()
341
+        cr.translate(0.5*rect.width, 0.5*rect.height)
342
+        cr.scale(self.zoom_ratio, self.zoom_ratio)
343
+        cr.translate(-self.x, -self.y)
344
+
345
+        self.graph.draw(cr, highlight_items=self.highlight)
346
+
347
+    def get_drag_action(self, event):
348
+        state = event.state
349
+        if event.button in (1, 2): # left or middle button
350
+            modifiers = Gtk.accelerator_get_default_mod_mask()
351
+            if state & modifiers == Gdk.ModifierType.CONTROL_MASK:
352
+                return ZoomAction
353
+            elif state & modifiers == Gdk.ModifierType.SHIFT_MASK:
354
+                return ZoomAreaAction
355
+            else:
356
+                return PanAction
357
+        return NullAction
358
+
359
+    def on_area_button_press(self, area, event):
360
+        self.animation.stop()
361
+        self.drag_action.abort()
362
+        action_type = self.get_drag_action(event)
363
+        self.drag_action = action_type(self)
364
+        self.drag_action.on_button_press(event)
365
+        self.presstime = time.time()
366
+        self.pressx = event.x
367
+        self.pressy = event.y
368
+        return False
369
+
370
+    def is_click(self, event, click_fuzz=4, click_timeout=1.0):
371
+        assert event.type == Gdk.EventType.BUTTON_RELEASE
372
+        if self.presstime is None:
373
+            # got a button release without seeing the press?
374
+            return False
375
+        # XXX instead of doing this complicated logic, shouldn't we listen
376
+        # for gtk's clicked event instead?
377
+        deltax = self.pressx - event.x
378
+        deltay = self.pressy - event.y
379
+        return (time.time() < self.presstime + click_timeout
380
+                and math.hypot(deltax, deltay) < click_fuzz)
381
+
382
+    def on_click(self, element, event):
383
+        """Override this method in subclass to process
384
+        click events. Note that element can be None
385
+        (click on empty space)."""
386
+        return False
387
+
388
+    def on_area_button_release(self, area, event):
389
+        self.drag_action.on_button_release(event)
390
+        self.drag_action = NullAction(self)
391
+        x, y = int(event.x), int(event.y)
392
+        if self.is_click(event):
393
+            el = self.get_element(x, y)
394
+            if self.on_click(el, event):
395
+                return True
396
+
397
+            if event.button == 1:
398
+                url = self.get_url(x, y)
399
+                if url is not None:
400
+                    self.emit('clicked', url.url, event)
401
+                else:
402
+                    jump = self.get_jump(x, y)
403
+                    if jump is not None:
404
+                        self.animate_to(jump.x, jump.y)
405
+
406
+                return True
407
+
408
+        if event.button == 1 or event.button == 2:
409
+            return True
410
+        return False
411
+
412
+    def on_area_scroll_event(self, area, event):
413
+        if event.direction == Gdk.ScrollDirection.UP:
414
+            self.zoom_image(self.zoom_ratio * self.ZOOM_INCREMENT,
415
+                            pos=(event.x, event.y))
416
+            return True
417
+        if event.direction == Gdk.ScrollDirection.DOWN:
418
+            self.zoom_image(self.zoom_ratio / self.ZOOM_INCREMENT,
419
+                            pos=(event.x, event.y))
420
+            return True
421
+        return False
422
+
423
+    def on_area_motion_notify(self, area, event):
424
+        self.drag_action.on_motion_notify(event)
425
+        return True
426
+
427
+    def on_area_size_allocate(self, area, allocation):
428
+        if self.zoom_to_fit_on_resize:
429
+            self.zoom_to_fit()
430
+
431
+    def animate_to(self, x, y):
432
+        self.animation = ZoomToAnimation(self, x, y)
433
+        self.animation.start()
434
+
435
+    def window2graph(self, x, y):
436
+        rect = self.get_allocation()
437
+        x -= 0.5*rect.width
438
+        y -= 0.5*rect.height
439
+        x /= self.zoom_ratio
440
+        y /= self.zoom_ratio
441
+        x += self.x
442
+        y += self.y
443
+        return x, y
444
+
445
+    def get_element(self, x, y):
446
+        x, y = self.window2graph(x, y)
447
+        return self.graph.get_element(x, y)
448
+
449
+    def get_url(self, x, y):
450
+        x, y = self.window2graph(x, y)
451
+        return self.graph.get_url(x, y)
452
+
453
+    def get_jump(self, x, y):
454
+        x, y = self.window2graph(x, y)
455
+        return self.graph.get_jump(x, y)
456
+
457
+
458
+class FindMenuToolAction(Gtk.Action):
459
+    __gtype_name__ = "FindMenuToolAction"
460
+
461
+    def do_create_tool_item(self):
462
+        return Gtk.ToolItem()
463
+
464
+
465
+class DotWindow(Gtk.Window):
466
+
467
+    ui = '''
468
+    <ui>
469
+        <toolbar name="ToolBar">
470
+            <toolitem action="Open"/>
471
+            <toolitem action="Reload"/>
472
+            <toolitem action="Print"/>
473
+            <separator/>
474
+            <toolitem action="ZoomIn"/>
475
+            <toolitem action="ZoomOut"/>
476
+            <toolitem action="ZoomFit"/>
477
+            <toolitem action="Zoom100"/>
478
+            <separator/>
479
+            <toolitem name="Find" action="Find"/>
480
+        </toolbar>
481
+    </ui>
482
+    '''
483
+
484
+    base_title = 'Dot Viewer'
485
+
486
+    def __init__(self, widget=None, width=512, height=512):
487
+        Gtk.Window.__init__(self)
488
+
489
+        self.graph = Graph()
490
+
491
+        window = self
492
+
493
+        window.set_title(self.base_title)
494
+        window.set_default_size(width, height)
495
+        vbox = Gtk.VBox()
496
+        window.add(vbox)
497
+
498
+        self.dotwidget = widget or DotWidget()
499
+        self.dotwidget.connect("error", lambda e, m: self.error_dialog(m))
500
+
501
+        # Create a UIManager instance
502
+        uimanager = self.uimanager = Gtk.UIManager()
503
+
504
+        # Add the accelerator group to the toplevel window
505
+        accelgroup = uimanager.get_accel_group()
506
+        window.add_accel_group(accelgroup)
507
+
508
+        # Create an ActionGroup
509
+        actiongroup = Gtk.ActionGroup('Actions')
510
+        self.actiongroup = actiongroup
511
+
512
+        # Create actions
513
+        actiongroup.add_actions((
514
+            ('Open', Gtk.STOCK_OPEN, None, None, None, self.on_open),
515
+            ('Reload', Gtk.STOCK_REFRESH, None, None, None, self.on_reload),
516
+            ('Print', Gtk.STOCK_PRINT, None, None, "Prints the currently visible part of the graph", self.dotwidget.on_print),
517
+            ('ZoomIn', Gtk.STOCK_ZOOM_IN, None, None, None, self.dotwidget.on_zoom_in),
518
+            ('ZoomOut', Gtk.STOCK_ZOOM_OUT, None, None, None, self.dotwidget.on_zoom_out),
519
+            ('ZoomFit', Gtk.STOCK_ZOOM_FIT, None, None, None, self.dotwidget.on_zoom_fit),
520
+            ('Zoom100', Gtk.STOCK_ZOOM_100, None, None, None, self.dotwidget.on_zoom_100),
521
+        ))
522
+
523
+        find_action = FindMenuToolAction("Find", None,
524
+                                          "Find a node by name", None)
525
+        actiongroup.add_action(find_action)
526
+
527
+        # Add the actiongroup to the uimanager
528
+        uimanager.insert_action_group(actiongroup, 0)
529
+
530
+        # Add a UI descrption
531
+        uimanager.add_ui_from_string(self.ui)
532
+
533
+        # Create a Toolbar
534
+        toolbar = uimanager.get_widget('/ToolBar')
535
+        vbox.pack_start(toolbar, False, False, 0)
536
+
537
+        vbox.pack_start(self.dotwidget, True, True, 0)
538
+
539
+        self.last_open_dir = "."
540
+
541
+        self.set_focus(self.dotwidget)
542
+
543
+        # Add Find text search
544
+        find_toolitem = uimanager.get_widget('/ToolBar/Find')
545
+        self.textentry = Gtk.Entry(max_length=20)
546
+        self.textentry.set_icon_from_stock(0, Gtk.STOCK_FIND)
547
+        find_toolitem.add(self.textentry)
548
+
549
+        self.textentry.set_activates_default(True)
550
+        self.textentry.connect ("activate", self.textentry_activate, self.textentry);
551
+        self.textentry.connect ("changed", self.textentry_changed, self.textentry);
552
+
553
+        self.show_all()
554
+
555
+    def find_text(self, entry_text):
556
+        found_items = []
557
+        dot_widget = self.dotwidget
558
+        regexp = re.compile(entry_text)
559
+        for element in dot_widget.graph.nodes + dot_widget.graph.edges:
560
+            if element.search_text(regexp):
561
+                found_items.append(element)
562
+        return found_items
563
+
564
+    def textentry_changed(self, widget, entry):
565
+        entry_text = entry.get_text()
566
+        dot_widget = self.dotwidget
567
+        if not entry_text:
568
+            dot_widget.set_highlight(None, search=True)
569
+            return
570
+
571
+        found_items = self.find_text(entry_text)
572
+        dot_widget.set_highlight(found_items, search=True)
573
+
574
+    def textentry_activate(self, widget, entry):
575
+        entry_text = entry.get_text()
576
+        dot_widget = self.dotwidget
577
+        if not entry_text:
578
+            dot_widget.set_highlight(None, search=True)
579
+            return;
580
+
581
+        found_items = self.find_text(entry_text)
582
+        dot_widget.set_highlight(found_items, search=True)
583
+        if(len(found_items) == 1):
584
+            dot_widget.animate_to(found_items[0].x, found_items[0].y)
585
+
586
+    def set_filter(self, filter):
587
+        self.dotwidget.set_filter(filter)
588
+
589
+    def set_dotcode(self, dotcode, filename=None):
590
+        if self.dotwidget.set_dotcode(dotcode, filename):
591
+            self.update_title(filename)
592
+            self.dotwidget.zoom_to_fit()
593
+
594
+    def set_xdotcode(self, xdotcode, filename=None):
595
+        if self.dotwidget.set_xdotcode(xdotcode):
596
+            self.update_title(filename)
597
+            self.dotwidget.zoom_to_fit()
598
+
599
+    def update_title(self, filename=None):
600
+        if filename is None:
601
+            self.set_title(self.base_title)
602
+        else:
603
+            self.set_title(os.path.basename(filename) + ' - ' + self.base_title)
604
+
605
+    def open_file(self, filename):
606
+        try:
607
+            fp = open(filename, 'rt')
608
+            self.set_dotcode(fp.read(), filename)
609
+            fp.close()
610
+        except IOError as ex:
611
+            self.error_dialog(str(ex))
612
+
613
+    def on_open(self, action):
614
+        chooser = Gtk.FileChooserDialog(parent=self,
615
+                                        title="Open dot File",
616
+                                        action=Gtk.FileChooserAction.OPEN,
617
+                                        buttons=(Gtk.STOCK_CANCEL,
618
+                                                 Gtk.ResponseType.CANCEL,
619
+                                                 Gtk.STOCK_OPEN,
620
+                                                 Gtk.ResponseType.OK))
621
+        chooser.set_default_response(Gtk.ResponseType.OK)
622
+        chooser.set_current_folder(self.last_open_dir)
623
+        filter = Gtk.FileFilter()
624
+        filter.set_name("Graphviz dot files")
625
+        filter.add_pattern("*.dot")
626
+        chooser.add_filter(filter)
627
+        filter = Gtk.FileFilter()
628
+        filter.set_name("All files")
629
+        filter.add_pattern("*")
630
+        chooser.add_filter(filter)
631
+        if chooser.run() == Gtk.ResponseType.OK:
632
+            filename = chooser.get_filename()
633
+            self.last_open_dir = chooser.get_current_folder()
634
+            chooser.destroy()
635
+            self.open_file(filename)
636
+        else:
637
+            chooser.destroy()
638
+
639
+    def on_reload(self, action):
640
+        self.dotwidget.reload()
641
+
642
+    def error_dialog(self, message):
643
+        dlg = Gtk.MessageDialog(parent=self,
644
+                                type=Gtk.MessageType.ERROR,
645
+                                message_format=message,
646
+                                buttons=Gtk.ButtonsType.OK)
647
+        dlg.set_title(self.base_title)
648
+        dlg.run()
649
+        dlg.destroy()