~ubuntu-branches/ubuntu/karmic/pypy/karmic

« back to all changes in this revision

Viewing changes to pypy/translator/tool/pygame/drawgraph.py

  • Committer: Bazaar Package Importer
  • Author(s): Alexandre Fayolle
  • Date: 2007-04-13 09:33:09 UTC
  • Revision ID: james.westby@ubuntu.com-20070413093309-yoojh4jcoocu2krz
Tags: upstream-1.0.0
ImportĀ upstreamĀ versionĀ 1.0.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""
 
2
A custom graphic renderer for the '.plain' files produced by dot.
 
3
 
 
4
"""
 
5
 
 
6
from __future__ import generators
 
7
import autopath
 
8
import re, os, math
 
9
import pygame
 
10
from pygame.locals import *
 
11
 
 
12
 
 
13
FONT = os.path.join(autopath.this_dir, 'cyrvetic.ttf')
 
14
FIXEDFONT = os.path.join(autopath.this_dir, 'VeraMoBd.ttf')
 
15
COLOR = {
 
16
    'black': (0,0,0),
 
17
    'white': (255,255,255),
 
18
    'red': (255,0,0),
 
19
    'green': (0,255,0),
 
20
    'blue': (0,0,255),
 
21
    'yellow': (255,255,0),
 
22
    }
 
23
re_nonword=re.compile(r'([^0-9a-zA-Z_.]+)')
 
24
 
 
25
def combine(color1, color2, alpha):
 
26
    r1, g1, b1 = color1
 
27
    r2, g2, b2 = color2
 
28
    beta = 1.0 - alpha
 
29
    return (int(r1 * alpha + r2 * beta),
 
30
            int(g1 * alpha + g2 * beta),
 
31
            int(b1 * alpha + b2 * beta))
 
32
 
 
33
 
 
34
def highlight_color(color):
 
35
    if color == (0, 0, 0): # black becomes magenta
 
36
        return (255, 0, 255)
 
37
    elif color == (255, 255, 255): # white becomes yellow
 
38
        return (255, 255, 0)
 
39
    intensity = sum(color)
 
40
    if intensity > 191 * 3:
 
41
        return combine(color, (128, 192, 0), 0.2)
 
42
    else:
 
43
        return combine(color, (255, 255, 0), 0.2)
 
44
 
 
45
def getcolor(name, default):
 
46
    if name in COLOR:
 
47
        return COLOR[name]
 
48
    elif name.startswith('#') and len(name) == 7:
 
49
        rval = COLOR[name] = (int(name[1:3],16), int(name[3:5],16), int(name[5:7],16))
 
50
        return rval
 
51
    else:
 
52
        return default
 
53
 
 
54
 
 
55
class GraphLayout:
 
56
    fixedfont = False
 
57
 
 
58
    def __init__(self, filename):
 
59
        # parse the layout file (.plain format)
 
60
        lines = open(str(filename), 'r').readlines()
 
61
        for i in range(len(lines)-2, -1, -1):
 
62
            if lines[i].endswith('\\\n'):   # line ending in '\'
 
63
                lines[i] = lines[i][:-2] + lines[i+1]
 
64
                del lines[i+1]
 
65
        header = splitline(lines.pop(0))
 
66
        # XXX very simple-minded way to give a somewhat better error message
 
67
        if header[0] == '<body':
 
68
            raise Exception("the dot on codespeak has very likely crashed")
 
69
        assert header[0] == 'graph'
 
70
        self.scale = float(header[1])
 
71
        self.boundingbox = float(header[2]), float(header[3])
 
72
        self.nodes = {}
 
73
        self.edges = []
 
74
        for line in lines:
 
75
            line = splitline(line)
 
76
            if line[0] == 'node':
 
77
                n = Node(*line[1:])
 
78
                self.nodes[n.name] = n
 
79
            if line[0] == 'edge':
 
80
                self.edges.append(Edge(self.nodes, *line[1:]))
 
81
            if line[0] == 'stop':
 
82
                break
 
83
        self.links = {}
 
84
 
 
85
    def get_display(self):
 
86
        from pypy.translator.tool.pygame.graphdisplay import GraphDisplay
 
87
        return GraphDisplay(self)      
 
88
 
 
89
    def display(self):
 
90
        self.get_display().run()
 
91
 
 
92
    def reload(self):
 
93
        return self
 
94
 
 
95
# async interaction helpers
 
96
 
 
97
def display_async_quit():
 
98
    pygame.event.post(pygame.event.Event(QUIT))        
 
99
 
 
100
def display_async_cmd(**kwds):                
 
101
    pygame.event.post(pygame.event.Event(USEREVENT, **kwds))
 
102
 
 
103
EventQueue = []
 
104
 
 
105
def wait_for_events():
 
106
    if not EventQueue:
 
107
        EventQueue.append(pygame.event.wait())
 
108
        EventQueue.extend(pygame.event.get())
 
109
 
 
110
def wait_for_async_cmd():
 
111
    # wait until another thread pushes a USEREVENT in the queue
 
112
    while True:
 
113
        wait_for_events()
 
114
        e = EventQueue.pop(0)
 
115
        if e.type in (USEREVENT, QUIT):   # discard all other events
 
116
            break
 
117
    EventQueue.insert(0, e)   # re-insert the event for further processing
 
118
 
 
119
 
 
120
class Node:
 
121
    def __init__(self, name, x, y, w, h, label, style, shape, color, fillcolor):
 
122
        self.name = name
 
123
        self.x = float(x)
 
124
        self.y = float(y)
 
125
        self.w = float(w)
 
126
        self.h = float(h)
 
127
        self.label = label
 
128
        self.style = style
 
129
        self.shape = shape
 
130
        self.color = color
 
131
        self.fillcolor = fillcolor
 
132
        self.highlight = False
 
133
 
 
134
    def sethighlight(self, which):
 
135
        self.highlight = bool(which)
 
136
 
 
137
class Edge:
 
138
    label = None
 
139
    
 
140
    def __init__(self, nodes, tail, head, cnt, *rest):
 
141
        self.tail = nodes[tail]
 
142
        self.head = nodes[head]
 
143
        cnt = int(cnt)
 
144
        self.points = [(float(rest[i]), float(rest[i+1]))
 
145
                       for i in range(0, cnt*2, 2)]
 
146
        rest = rest[cnt*2:]
 
147
        if len(rest) > 2:
 
148
            self.label, xl, yl = rest[:3]
 
149
            self.xl = float(xl)
 
150
            self.yl = float(yl)
 
151
            rest = rest[3:]
 
152
        self.style, self.color = rest
 
153
        self.highlight = False
 
154
        self.cachedbezierpoints = None
 
155
        self.cachedarrowhead = None
 
156
        self.cachedlimits = None
 
157
 
 
158
    def sethighlight(self, which):
 
159
        self.highlight = bool(which)
 
160
 
 
161
    def limits(self):
 
162
        result = self.cachedlimits
 
163
        if result is None:
 
164
            points = self.bezierpoints()
 
165
            xs = [point[0] for point in points]
 
166
            ys = [point[1] for point in points]
 
167
            self.cachedlimits = result = (min(xs), max(ys), max(xs), min(ys))
 
168
        return result
 
169
 
 
170
    def bezierpoints(self):
 
171
        result = self.cachedbezierpoints
 
172
        if result is None:
 
173
            result = []
 
174
            pts = self.points
 
175
            for i in range(0, len(pts)-3, 3):
 
176
                result += beziercurve(pts[i], pts[i+1], pts[i+2], pts[i+3])
 
177
            self.cachedbezierpoints = result
 
178
        return result
 
179
 
 
180
    def arrowhead(self):
 
181
        result = self.cachedarrowhead
 
182
        if result is None:
 
183
            bottom_up = self.points[0][1] > self.points[-1][1]
 
184
            if (self.tail.y > self.head.y) != bottom_up:   # reversed edge
 
185
                head = 0
 
186
                dir = 1
 
187
            else:
 
188
                head = -1
 
189
                dir = -1
 
190
            n = 1
 
191
            while True:
 
192
                try:
 
193
                    x0, y0 = self.points[head]
 
194
                    x1, y1 = self.points[head+n*dir]
 
195
                except IndexError:
 
196
                    result = []
 
197
                    break
 
198
                vx = x0-x1
 
199
                vy = y0-y1
 
200
                try:
 
201
                    f = 0.12 / math.sqrt(vx*vx + vy*vy)
 
202
                    vx *= f
 
203
                    vy *= f
 
204
                    result = [(x0 + 0.9*vx, y0 + 0.9*vy),
 
205
                              (x0 + 0.4*vy, y0 - 0.4*vx),
 
206
                              (x0 - 0.4*vy, y0 + 0.4*vx)]
 
207
                    break
 
208
                except (ZeroDivisionError, ValueError):
 
209
                    n += 1
 
210
            self.cachedarrowhead = result
 
211
        return result
 
212
 
 
213
def beziercurve((x0,y0), (x1,y1), (x2,y2), (x3,y3), resolution=8):
 
214
    result = []
 
215
    f = 1.0/(resolution-1)
 
216
    append = result.append
 
217
    for i in range(resolution):
 
218
        t = f*i
 
219
        t0 = (1-t)*(1-t)*(1-t)
 
220
        t1 =   t  *(1-t)*(1-t) * 3.0
 
221
        t2 =   t  *  t  *(1-t) * 3.0
 
222
        t3 =   t  *  t  *  t
 
223
        append((x0*t0 + x1*t1 + x2*t2 + x3*t3,
 
224
                y0*t0 + y1*t1 + y2*t2 + y3*t3))
 
225
    return result
 
226
 
 
227
def segmentdistance((x0,y0), (x1,y1), (x,y)):
 
228
    "Distance between the point (x,y) and the segment (x0,y0)-(x1,y1)."
 
229
    vx = x1-x0
 
230
    vy = y1-y0
 
231
    try:
 
232
        l = math.hypot(vx, vy)
 
233
        vx /= l
 
234
        vy /= l
 
235
        dlong = vx*(x-x0) + vy*(y-y0)
 
236
    except (ZeroDivisionError, ValueError):
 
237
        dlong = -1
 
238
    if dlong < 0.0:
 
239
        return math.hypot(x-x0, y-y0)
 
240
    elif dlong > l:
 
241
        return math.hypot(x-x1, y-y1)
 
242
    else:
 
243
        return abs(vy*(x-x0) - vx*(y-y0))
 
244
 
 
245
def splitline(line, re_word = re.compile(r'[^\s"]\S*|["]["]|["].*?[^\\]["]')):
 
246
    result = []
 
247
    for word in re_word.findall(line):
 
248
        if word.startswith('"'):
 
249
            word = eval(word)
 
250
        result.append(word)
 
251
    return result
 
252
 
 
253
 
 
254
class GraphRenderer:
 
255
    MARGIN = 0.6
 
256
    SCALEMIN = 3
 
257
    SCALEMAX = 100
 
258
    FONTCACHE = {}
 
259
    
 
260
    def __init__(self, screen, graphlayout, scale=75):
 
261
        self.graphlayout = graphlayout
 
262
        self.setscale(scale)
 
263
        self.setoffset(0, 0)
 
264
        self.screen = screen
 
265
        self.textzones = []
 
266
        self.highlightwords = graphlayout.links
 
267
        self.highlight_word = None
 
268
        self.visiblenodes = []
 
269
        self.visibleedges = []
 
270
 
 
271
    def wordcolor(self, word):
 
272
        info = self.highlightwords[word]
 
273
        if isinstance(info, tuple) and len(info) >= 2:
 
274
            color = info[1]
 
275
        else:
 
276
            color = None
 
277
        if color is None:
 
278
            color = (128,0,0)
 
279
        if word == self.highlight_word:
 
280
            return ((255,255,80), color)
 
281
        else:
 
282
            return (color, None)
 
283
 
 
284
    def setscale(self, scale):
 
285
        scale = max(min(scale, self.SCALEMAX), self.SCALEMIN)
 
286
        self.scale = float(scale)
 
287
        w, h = self.graphlayout.boundingbox
 
288
        self.margin = int(self.MARGIN * scale)
 
289
        self.width = int(w * scale) + (2 * self.margin)
 
290
        self.height = int(h * scale) + (2 * self.margin)
 
291
        self.bboxh = h
 
292
        size = int(15 * (scale-10) / 75)
 
293
        self.font = self.getfont(size)
 
294
 
 
295
    def getfont(self, size):
 
296
        if size in self.FONTCACHE:
 
297
            return self.FONTCACHE[size]
 
298
        elif size < 5:
 
299
            self.FONTCACHE[size] = None
 
300
            return None
 
301
        else:
 
302
            if self.graphlayout.fixedfont:
 
303
                filename = FIXEDFONT
 
304
            else:
 
305
                filename = FONT
 
306
            font = self.FONTCACHE[size] = pygame.font.Font(filename, size)
 
307
            return font
 
308
    
 
309
    def setoffset(self, offsetx, offsety):
 
310
        "Set the (x,y) origin of the rectangle where the graph will be rendered."
 
311
        self.ofsx = offsetx - self.margin
 
312
        self.ofsy = offsety - self.margin
 
313
 
 
314
    def shiftoffset(self, dx, dy):
 
315
        self.ofsx += dx
 
316
        self.ofsy += dy
 
317
 
 
318
    def getcenter(self):
 
319
        w, h = self.screen.get_size()
 
320
        return self.revmap(w//2, h//2)
 
321
 
 
322
    def setcenter(self, x, y):
 
323
        w, h = self.screen.get_size()
 
324
        x, y = self.map(x, y)
 
325
        self.shiftoffset(x-w//2, y-h//2)
 
326
 
 
327
    def shiftscale(self, factor, fix=None):
 
328
        if fix is None:
 
329
            fixx, fixy = self.screen.get_size()
 
330
            fixx //= 2
 
331
            fixy //= 2
 
332
        else:
 
333
            fixx, fixy = fix
 
334
        x, y = self.revmap(fixx, fixy)
 
335
        self.setscale(self.scale * factor)
 
336
        newx, newy = self.map(x, y)
 
337
        self.shiftoffset(newx - fixx, newy - fixy)
 
338
 
 
339
    def reoffset(self, swidth, sheight):
 
340
        offsetx = noffsetx = self.ofsx
 
341
        offsety = noffsety = self.ofsy
 
342
        width = self.width
 
343
        height = self.height
 
344
 
 
345
        # if it fits, center it, otherwise clamp
 
346
        if width <= swidth:
 
347
            noffsetx = (width - swidth) // 2
 
348
        else:
 
349
            noffsetx = min(max(0, offsetx), width - swidth)
 
350
 
 
351
        if height <= sheight:
 
352
            noffsety = (height - sheight) // 2
 
353
        else:
 
354
            noffsety = min(max(0, offsety), height - sheight)
 
355
 
 
356
        self.ofsx = noffsetx
 
357
        self.ofsy = noffsety
 
358
 
 
359
    def getboundingbox(self):
 
360
        "Get the rectangle where the graph will be rendered."
 
361
        return (-self.ofsx, -self.ofsy, self.width, self.height)
 
362
 
 
363
    def visible(self, x1, y1, x2, y2):
 
364
        """Is any part of the box visible (i.e. within the bounding box)?
 
365
 
 
366
        We have to perform clipping ourselves because with big graphs the
 
367
        coordinates may sometimes become longs and cause OverflowErrors
 
368
        within pygame.
 
369
        """
 
370
        w, h = self.screen.get_size()
 
371
        return x1 < w and x2 > 0 and y1 < h and y2 > 0
 
372
 
 
373
    def computevisible(self):
 
374
        del self.visiblenodes[:]
 
375
        del self.visibleedges[:]
 
376
        w, h = self.screen.get_size()
 
377
        for node in self.graphlayout.nodes.values():
 
378
            x, y = self.map(node.x, node.y)
 
379
            nw2 = int(node.w * self.scale)//2
 
380
            nh2 = int(node.h * self.scale)//2
 
381
            if x-nw2 < w and x+nw2 > 0 and y-nh2 < h and y+nh2 > 0:
 
382
                self.visiblenodes.append(node)
 
383
        for edge in self.graphlayout.edges:
 
384
            x1, y1, x2, y2 = edge.limits()
 
385
            x1, y1 = self.map(x1, y1)
 
386
            if x1 < w and y1 < h:
 
387
                x2, y2 = self.map(x2, y2)
 
388
                if x2 > 0 and y2 > 0:
 
389
                    self.visibleedges.append(edge)
 
390
 
 
391
    def map(self, x, y):
 
392
        return (int(x*self.scale) - (self.ofsx - self.margin),
 
393
                int((self.bboxh-y)*self.scale) - (self.ofsy - self.margin))
 
394
 
 
395
    def revmap(self, px, py):
 
396
        return ((px + (self.ofsx - self.margin)) / self.scale,
 
397
                self.bboxh - (py + (self.ofsy - self.margin)) / self.scale)
 
398
 
 
399
    def draw_node_commands(self, node):
 
400
        xcenter, ycenter = self.map(node.x, node.y)
 
401
        boxwidth = int(node.w * self.scale)
 
402
        boxheight = int(node.h * self.scale)
 
403
        fgcolor = getcolor(node.color, (0,0,0))
 
404
        bgcolor = getcolor(node.fillcolor, (255,255,255))
 
405
        if node.highlight:
 
406
            fgcolor = highlight_color(fgcolor)
 
407
            bgcolor = highlight_color(bgcolor)
 
408
 
 
409
        text = node.label
 
410
        lines = text.replace('\\l','\\l\n').replace('\r','\r\n').split('\n')
 
411
        # ignore a final newline
 
412
        if not lines[-1]:
 
413
            del lines[-1]
 
414
        wmax = 0
 
415
        hmax = 0
 
416
        commands = []
 
417
        bkgndcommands = []
 
418
 
 
419
        if self.font is None:
 
420
            if lines:
 
421
                raw_line = lines[0].replace('\\l','').replace('\r','')
 
422
                if raw_line:
 
423
                    for size in (12, 10, 8, 6, 4):
 
424
                        font = self.getfont(size)
 
425
                        img = TextSnippet(self, raw_line, (0, 0, 0), bgcolor, font=font)
 
426
                        w, h = img.get_size()
 
427
                        if (w >= boxwidth or h >= boxheight):
 
428
                            continue
 
429
                        else:
 
430
                            if w>wmax: wmax = w
 
431
                            def cmd(img=img, y=hmax, w=w):
 
432
                                img.draw(xcenter-w//2, ytop+y)
 
433
                            commands.append(cmd)
 
434
                            hmax += h
 
435
                            break
 
436
        else:
 
437
            for line in lines:
 
438
                raw_line = line.replace('\\l','').replace('\r','') or ' '
 
439
                img = TextSnippet(self, raw_line, (0, 0, 0), bgcolor)
 
440
                w, h = img.get_size()
 
441
                if w>wmax: wmax = w
 
442
                if raw_line.strip():
 
443
                    if line.endswith('\\l'):
 
444
                        def cmd(img=img, y=hmax):
 
445
                            img.draw(xleft, ytop+y)
 
446
                    elif line.endswith('\r'):
 
447
                        def cmd(img=img, y=hmax, w=w):
 
448
                            img.draw(xright-w, ytop+y)
 
449
                    else:
 
450
                        def cmd(img=img, y=hmax, w=w):
 
451
                            img.draw(xcenter-w//2, ytop+y)
 
452
                    commands.append(cmd)
 
453
                hmax += h
 
454
                #hmax += 8
 
455
 
 
456
        # we know the bounding box only now; setting these variables will
 
457
        # have an effect on the values seen inside the cmd() functions above
 
458
        xleft = xcenter - wmax//2
 
459
        xright = xcenter + wmax//2
 
460
        ytop = ycenter - hmax//2
 
461
        x = xcenter-boxwidth//2
 
462
        y = ycenter-boxheight//2
 
463
 
 
464
        if node.shape == 'box':
 
465
            rect = (x-1, y-1, boxwidth+2, boxheight+2)
 
466
            def cmd():
 
467
                self.screen.fill(bgcolor, rect)
 
468
            bkgndcommands.append(cmd)
 
469
            def cmd():
 
470
                pygame.draw.rect(self.screen, fgcolor, rect, 1)
 
471
            commands.append(cmd)
 
472
        elif node.shape == 'ellipse':
 
473
            rect = (x-1, y-1, boxwidth+2, boxheight+2)
 
474
            def cmd():
 
475
                pygame.draw.ellipse(self.screen, bgcolor, rect, 0)
 
476
            bkgndcommands.append(cmd)
 
477
            def cmd():
 
478
                pygame.draw.ellipse(self.screen, fgcolor, rect, 1)
 
479
            commands.append(cmd)
 
480
        elif node.shape == 'octagon':
 
481
            step = 1-math.sqrt(2)/2
 
482
            points = [(int(x+boxwidth*fx), int(y+boxheight*fy))
 
483
                      for fx, fy in [(step,0), (1-step,0),
 
484
                                     (1,step), (1,1-step),
 
485
                                     (1-step,1), (step,1),
 
486
                                     (0,1-step), (0,step)]]
 
487
            def cmd():
 
488
                pygame.draw.polygon(self.screen, bgcolor, points, 0)
 
489
            bkgndcommands.append(cmd)
 
490
            def cmd():
 
491
                pygame.draw.polygon(self.screen, fgcolor, points, 1)
 
492
            commands.append(cmd)
 
493
        return bkgndcommands, commands
 
494
 
 
495
    def draw_commands(self):
 
496
        nodebkgndcmd = []
 
497
        nodecmd = []
 
498
        for node in self.visiblenodes:
 
499
            cmd1, cmd2 = self.draw_node_commands(node)
 
500
            nodebkgndcmd += cmd1
 
501
            nodecmd += cmd2
 
502
 
 
503
        edgebodycmd = []
 
504
        edgeheadcmd = []
 
505
        for edge in self.visibleedges:
 
506
 
 
507
            fgcolor = getcolor(edge.color, (0,0,0))
 
508
            if edge.highlight:
 
509
                fgcolor = highlight_color(fgcolor)
 
510
            points = [self.map(*xy) for xy in edge.bezierpoints()]
 
511
 
 
512
            def drawedgebody(points=points, fgcolor=fgcolor):
 
513
                pygame.draw.lines(self.screen, fgcolor, False, points)
 
514
            edgebodycmd.append(drawedgebody)
 
515
 
 
516
            points = [self.map(*xy) for xy in edge.arrowhead()]
 
517
            if points:
 
518
                def drawedgehead(points=points, fgcolor=fgcolor):
 
519
                    pygame.draw.polygon(self.screen, fgcolor, points, 0)
 
520
                edgeheadcmd.append(drawedgehead)
 
521
 
 
522
            if edge.label:
 
523
                x, y = self.map(edge.xl, edge.yl)
 
524
                img = TextSnippet(self, edge.label, (0, 0, 0))
 
525
                w, h = img.get_size()
 
526
                if self.visible(x-w//2, y-h//2, x+w//2, y+h//2):
 
527
                    def drawedgelabel(img=img, x1=x-w//2, y1=y-h//2):
 
528
                        img.draw(x1, y1)
 
529
                    edgeheadcmd.append(drawedgelabel)
 
530
 
 
531
        return edgebodycmd + nodebkgndcmd + edgeheadcmd + nodecmd
 
532
 
 
533
    def render(self):
 
534
        self.computevisible()
 
535
 
 
536
        bbox = self.getboundingbox()
 
537
        ox, oy, width, height = bbox
 
538
        dpy_width, dpy_height = self.screen.get_size()
 
539
        # some versions of the SDL misinterpret widely out-of-range values,
 
540
        # so clamp them
 
541
        if ox < 0:
 
542
            width += ox
 
543
            ox = 0
 
544
        if oy < 0:
 
545
            height += oy
 
546
            oy = 0
 
547
        if width > dpy_width:
 
548
            width = dpy_width
 
549
        if height > dpy_height:
 
550
            height = dpy_height
 
551
        self.screen.fill((224, 255, 224), (ox, oy, width, height))
 
552
 
 
553
        # gray off-bkgnd areas
 
554
        gray = (128, 128, 128)
 
555
        if ox > 0:
 
556
            self.screen.fill(gray, (0, 0, ox, dpy_height))
 
557
        if oy > 0:
 
558
            self.screen.fill(gray, (0, 0, dpy_width, oy))
 
559
        w = dpy_width - (ox + width)
 
560
        if w > 0:
 
561
            self.screen.fill(gray, (dpy_width-w, 0, w, dpy_height))
 
562
        h = dpy_height - (oy + height)
 
563
        if h > 0:
 
564
            self.screen.fill(gray, (0, dpy_height-h, dpy_width, h))
 
565
 
 
566
        # draw the graph and record the position of texts
 
567
        del self.textzones[:]
 
568
        for cmd in self.draw_commands():
 
569
            cmd()
 
570
 
 
571
    def findall(self, searchstr):
 
572
        """Return an iterator for all nodes and edges that contain a searchstr.
 
573
        """
 
574
        for item in self.graphlayout.nodes.itervalues():
 
575
            if item.label and searchstr in item.label:
 
576
                yield item
 
577
        for item in self.graphlayout.edges:
 
578
            if item.label and searchstr in item.label:
 
579
                yield item
 
580
 
 
581
    def at_position(self, (x, y)):
 
582
        """Figure out the word under the cursor."""
 
583
        for rx, ry, rw, rh, word in self.textzones:
 
584
            if rx <= x < rx+rw and ry <= y < ry+rh:
 
585
                return word
 
586
        return None
 
587
 
 
588
    def node_at_position(self, (x, y)):
 
589
        """Return the Node under the cursor."""
 
590
        x, y = self.revmap(x, y)
 
591
        for node in self.visiblenodes:
 
592
            if 2.0*abs(x-node.x) <= node.w and 2.0*abs(y-node.y) <= node.h:
 
593
                return node
 
594
        return None
 
595
 
 
596
    def edge_at_position(self, (x, y), distmax=14):
 
597
        """Return the Edge near the cursor."""
 
598
        # XXX this function is very CPU-intensive and makes the display kinda sluggish
 
599
        distmax /= self.scale
 
600
        xy = self.revmap(x, y)
 
601
        closest_edge = None
 
602
        for edge in self.visibleedges:
 
603
            pts = edge.bezierpoints()
 
604
            for i in range(1, len(pts)):
 
605
                d = segmentdistance(pts[i-1], pts[i], xy)
 
606
                if d < distmax:
 
607
                    distmax = d
 
608
                    closest_edge = edge
 
609
        return closest_edge
 
610
 
 
611
 
 
612
class TextSnippet:
 
613
    
 
614
    def __init__(self, renderer, text, fgcolor, bgcolor=None, font=None):
 
615
        self.renderer = renderer
 
616
        self.imgs = []
 
617
        self.parts = []
 
618
        if font is None:
 
619
            font = renderer.font
 
620
        if font is None:
 
621
            return
 
622
        parts = self.parts
 
623
        for word in re_nonword.split(text):
 
624
            if not word:
 
625
                continue
 
626
            if word in renderer.highlightwords:
 
627
                fg, bg = renderer.wordcolor(word)
 
628
                bg = bg or bgcolor
 
629
            else:
 
630
                fg, bg = fgcolor, bgcolor
 
631
            parts.append((word, fg, bg))
 
632
        # consolidate sequences of words with the same color
 
633
        for i in range(len(parts)-2, -1, -1):
 
634
            if parts[i][1:] == parts[i+1][1:]:
 
635
                word, fg, bg = parts[i]
 
636
                parts[i] = word + parts[i+1][0], fg, bg
 
637
                del parts[i+1]
 
638
        # delete None backgrounds
 
639
        for i in range(len(parts)):
 
640
            if parts[i][2] is None:
 
641
                parts[i] = parts[i][:2]
 
642
        # render parts
 
643
        i = 0
 
644
        while i < len(parts):
 
645
            part = parts[i]
 
646
            word = part[0]
 
647
            try:
 
648
                try:
 
649
                    img = font.render(word, False, *part[1:])
 
650
                except pygame.error, e:
 
651
                    # Try *with* anti-aliasing to work around a bug in SDL
 
652
                    img = font.render(word, True, *part[1:])
 
653
            except pygame.error:
 
654
                del parts[i]   # Text has zero width
 
655
            else:
 
656
                self.imgs.append(img)
 
657
                i += 1
 
658
 
 
659
    def get_size(self):
 
660
        if self.imgs:
 
661
            sizes = [img.get_size() for img in self.imgs]
 
662
            return sum([w for w,h in sizes]), max([h for w,h in sizes])
 
663
        else:
 
664
            return 0, 0
 
665
 
 
666
    def draw(self, x, y):
 
667
        for part, img in zip(self.parts, self.imgs):
 
668
            word = part[0]
 
669
            self.renderer.screen.blit(img, (x, y))
 
670
            w, h = img.get_size()
 
671
            self.renderer.textzones.append((x, y, w, h, word))
 
672
            x += w
 
673
 
 
674
 
 
675
try:
 
676
    sum   # 2.3 only
 
677
except NameError:
 
678
    def sum(lst):
 
679
        total = 0
 
680
        for item in lst:
 
681
            total += lst
 
682
        return total