Web · Wiki · Activities · Blog · Lists · Chat · Meeting · Bugs · Git · Translate · Archive · People · Donate
1
# -*- coding: utf-8 -*-
2
3
#Copyright (c) 2007-8, Playful Invention Company.
4
#Copyright (c) 2008-11 Walter Bender
5
6
#Permission is hereby granted, free of charge, to any person obtaining a copy
7
#of this software and associated documentation files (the "Software"), to deal
8
#in the Software without restriction, including without limitation the rights
9
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
#copies of the Software, and to permit persons to whom the Software is
11
#furnished to do so, subject to the following conditions:
12
13
#The above copyright notice and this permission notice shall be included in
14
#all copies or substantial portions of the Software.
15
16
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
#THE SOFTWARE.
23
24
'''
25
26
sprites.py is a simple sprites library for managing graphics objects,
27
'sprites', on a gtk.DrawingArea. It manages multiple sprites with
28
methods such as move, hide, set_layer, etc.
29
30
There are two classes:
31
32
class Sprites maintains a collection of sprites
33
class Sprite manages individual sprites within the collection.
34
35
Example usage:
36
        # Import the classes into your program.
37
        from sprites import Sprites Sprite
38
39
        # Create a new sprite collection associated with your widget
40
        self.sprite_list = Sprites(widget)
41
42
        # Create a "pixbuf" (in this example, from SVG).
43
        my_pixbuf = svg_str_to_pixbuf("<svg>...some svg code...</svg>")
44
45
        # Create a sprite at position x1, y1.
46
        my_sprite = sprites.Sprite(self.sprite_list, x1, y1, my_pixbuf)
47
48
        # Move the sprite to a new position.
49
        my_sprite.move((x1+dx, y1+dy))
50
51
        # Create another "pixbuf".
52
        your_pixbuf = svg_str_to_pixbuf("<svg>...some svg code...</svg>")
53
54
        # Create a sprite at position x2, y2.
55
        your_sprite = sprites.Sprite(self.sprite_list, x2, y2, my_pixbuf)
56
57
        # Assign the sprites to layers.
58
        # In this example, your_sprite will be on top of my_sprite.
59
        my_sprite.set_layer(100)
60
        your_sprite.set_layer(200)
61
62
        # Now put my_sprite on top of your_sprite.
63
        my_sprite.set_layer(300)
64
65
        cr = self.window.cairo_create()
66
        # In your activity's do_expose_event, put in a call to redraw_sprites
67
        self.sprites.redraw_sprites(event.area, cairo_context)
68
69
# method for converting SVG to a gtk pixbuf
70
def svg_str_to_pixbuf(svg_string):
71
    pl = gtk.gdk.PixbufLoader('svg')
72
    pl.write(svg_string)
73
    pl.close()
74
    pixbuf = pl.get_pixbuf()
75
    return pixbuf
76
77
'''
78
79
import pygtk
80
pygtk.require('2.0')
81
import gtk
82
import pango
83
import pangocairo
84
import cairo
85
86
87
class Sprites:
88
    ''' A class for the list of sprites and everything they share in common '''
89
90
    def __init__(self, widget):
91
        ''' Initialize an empty array of sprites '''
92
        self.widget = widget
93
        self.list = []
94
        self.cr = None
95
96
    def set_cairo_context(self, cr):
97
        ''' Cairo context may be set or reset after __init__ '''
98
        self.cr = cr
99
100
    def get_sprite(self, i):
101
        ''' Return a sprint from the array '''
102
        if i < 0 or i > len(self.list) - 1:
103
            return(None)
104
        else:
105
            return(self.list[i])
106
107
    def length_of_list(self):
108
        ''' How many sprites are there? '''
109
        return len(self.list)
110
111
    def append_to_list(self, spr):
112
        ''' Append a new sprite to the end of the list. '''
113
        self.list.append(spr)
114
115
    def insert_in_list(self, spr, i):
116
        ''' Insert a sprite at position i. '''
117
        if i < 0:
118
            self.list.insert(0, spr)
119
        elif i > len(self.list) - 1:
120
            self.list.append(spr)
121
        else:
122
            self.list.insert(i, spr)
123
124
    def find_in_list(self, spr):
125
        return (spr in self.list)
126
127
    def remove_from_list(self, spr):
128
        ''' Remove a sprite from the list. '''
129
        if spr in self.list:
130
            self.list.remove(spr)
131
132
    def find_sprite(self, pos, region=False):
133
        ''' Search based on (x, y) position. Return the 'top/first' one. '''
134
        list = self.list[:]
135
        list.reverse()
136
        for spr in list:
137
            if spr.hit(pos, readpixel=not region):
138
                return spr
139
        return None
140
141
    def redraw_sprites(self, area=None, cr=None):
142
        ''' Redraw the sprites that intersect area. '''
143
        # I think I need to do this to save Cairo some work
144
        if cr is None:
145
            cr = self.cr
146
        else:
147
            self.cr = cr
148
        if cr is None:
149
            print 'sprites.redraw_sprites: no Cairo context'
150
            return
151
        for spr in self.list:
152
            if area is None:
153
                spr.draw(cr=cr)
154
            else:
155
                intersection = spr.rect.intersect(area)
156
                if intersection.width > 0 or intersection.height > 0:
157
                    spr.draw(cr=cr)
158
159
160
class Sprite:
161
    ''' A class for the individual sprites '''
162
163
    def __init__(self, sprites, x, y, image):
164
        ''' Initialize an individual sprite '''
165
        self._sprites = sprites
166
        self.save_xy = (x, y)  # remember initial (x, y) position
167
        self.rect = gtk.gdk.Rectangle(int(x), int(y), 0, 0)
168
        self._scale = [12]
169
        self._rescale = [True]
170
        self._horiz_align = ['center']
171
        self._vert_align = ['middle']
172
        self._x_pos = [None]
173
        self._y_pos = [None]
174
        self._fd = None
175
        self._bold = False
176
        self._italic = False
177
        self._color = None
178
        self._margins = [0, 0, 0, 0]
179
        self.layer = 100
180
        self.labels = []
181
        self.cached_surfaces = []
182
        self._dx = []  # image offsets
183
        self._dy = []
184
        self.type = None
185
        self.set_image(image)
186
        self._sprites.append_to_list(self)
187
188
    def set_image(self, image, i=0, dx=0, dy=0):
189
        ''' Add an image to the sprite. '''
190
        while len(self.cached_surfaces) < i + 1:
191
            self.cached_surfaces.append(None)
192
            self._dx.append(0)
193
            self._dy.append(0)
194
        self._dx[i] = dx
195
        self._dy[i] = dy
196
        if isinstance(image, gtk.gdk.Pixbuf) or \
197
           isinstance(image, cairo.ImageSurface):
198
            w = image.get_width()
199
            h = image.get_height()
200
        else:
201
            w, h = image.get_size()
202
        if i == 0:  # Always reset width and height when base image changes.
203
            self.rect.width = w + dx
204
            self.rect.height = h + dy
205
        else:
206
            if w + dx > self.rect.width:
207
                self.rect.width = w + dx
208
            if h + dy > self.rect.height:
209
                self.rect.height = h + dy
210
        if isinstance(image, cairo.ImageSurface):
211
            self.cached_surfaces[i] = image
212
        else:  # Convert to Cairo surface
213
            surface = cairo.ImageSurface(
214
                cairo.FORMAT_ARGB32, self.rect.width, self.rect.height)
215
            context = cairo.Context(surface)
216
            context = gtk.gdk.CairoContext(context)
217
            context.set_source_pixbuf(image, 0, 0)
218
            context.rectangle(0, 0, self.rect.width, self.rect.height)
219
            context.fill()
220
            self.cached_surfaces[i] = surface
221
222
    def move(self, pos):
223
        ''' Move to new (x, y) position '''
224
        self.inval()
225
        self.rect.x, self.rect.y = int(pos[0]), int(pos[1])
226
        self.inval()
227
228
    def move_relative(self, pos):
229
        ''' Move to new (x+dx, y+dy) position '''
230
        self.inval()
231
        self.rect.x += int(pos[0])
232
        self.rect.y += int(pos[1])
233
        self.inval()
234
235
    def get_xy(self):
236
        ''' Return current (x, y) position '''
237
        return (self.rect.x, self.rect.y)
238
239
    def get_dimensions(self):
240
        ''' Return current size '''
241
        return (self.rect.width, self.rect.height)
242
243
    def get_layer(self):
244
        ''' Return current layer '''
245
        return self.layer
246
247
    def set_shape(self, image, i=0):
248
        ''' Set the current image associated with the sprite '''
249
        self.inval()
250
        self.set_image(image, i)
251
        self.inval()
252
253
    def set_layer(self, layer=None):
254
        ''' Set the layer for a sprite '''
255
        self._sprites.remove_from_list(self)
256
        if layer is not None:
257
            self.layer = layer
258
        for i in range(self._sprites.length_of_list()):
259
            spr = self._sprites.get_sprite(i)
260
            if spr is not None and self.layer < spr.layer:
261
                self._sprites.insert_in_list(self, i)
262
                self.inval()
263
                return
264
        self._sprites.append_to_list(self)
265
        self.inval()
266
267
    def set_label(self, new_label, i=0):
268
        ''' Set the label drawn on the sprite '''
269
        self._extend_labels_array(i)
270
        if isinstance(new_label, (str, unicode)):
271
            # pango doesn't like nulls
272
            self.labels[i] = new_label.replace('\0', ' ')
273
        else:
274
            self.labels[i] = str(new_label)
275
        self.inval()
276
277
    def set_margins(self, l=0, t=0, r=0, b=0):
278
        ''' Set the margins for drawing the label '''
279
        self._margins = [l, t, r, b]
280
281
    def _extend_labels_array(self, i):
282
        ''' Append to the labels attribute list '''
283
        if self._fd is None:
284
            self.set_font('Sans')
285
        if self._color is None:
286
            self._color = (0., 0., 0.)
287
        while len(self.labels) < i + 1:
288
            self.labels.append(' ')
289
            self._scale.append(self._scale[0])
290
            self._rescale.append(self._rescale[0])
291
            self._horiz_align.append(self._horiz_align[0])
292
            self._vert_align.append(self._vert_align[0])
293
            self._x_pos.append(self._x_pos[0])
294
            self._y_pos.append(self._y_pos[0])
295
296
    def set_font(self, font):
297
        ''' Set the font for a label '''
298
        self._fd = pango.FontDescription(font)
299
300
    def set_label_color(self, rgb):
301
        ''' Set the font color for a label '''
302
        COLORTABLE = {'black': '#000000', 'white': '#FFFFFF',
303
                      'red': '#FF0000', 'yellow': '#FFFF00',
304
                      'green': '#00FF00', 'cyan': '#00FFFF',
305
                      'blue': '#0000FF', 'purple': '#FF00FF',
306
                      'gray': '#808080'}
307
        if rgb.lower() in COLORTABLE:
308
            rgb = COLORTABLE[rgb.lower()]
309
        # Convert from '#RRGGBB' to floats
310
        self._color = (int('0x' + rgb[1:3], 16) / 256.,
311
                       int('0x' + rgb[3:5], 16) / 256.,
312
                       int('0x' + rgb[5:7], 16) / 256.)
313
        return
314
315
    def set_label_attributes(self, scale, rescale=True, horiz_align='center',
316
                             vert_align='middle', x_pos=None, y_pos=None, i=0):
317
        ''' Set the various label attributes '''
318
        self._extend_labels_array(i)
319
        self._scale[i] = scale
320
        self._rescale[i] = rescale
321
        self._horiz_align[i] = horiz_align
322
        self._vert_align[i] = vert_align
323
        self._x_pos[i] = x_pos
324
        self._y_pos[i] = y_pos
325
326
    def hide(self):
327
        ''' Hide a sprite '''
328
        self.inval()
329
        self._sprites.remove_from_list(self)
330
331
    def restore(self):
332
        ''' Restore a hidden sprite '''
333
        self.set_layer()
334
335
    def inval(self):
336
        ''' Invalidate a region for gtk '''
337
        self._sprites.widget.queue_draw_area(self.rect.x,
338
                                             self.rect.y,
339
                                             self.rect.width,
340
                                             self.rect.height)
341
342
    def draw(self, cr=None):
343
        ''' Draw the sprite (and label) '''
344
        if cr is None:
345
            print 'sprite.draw: no Cairo context.'
346
            return
347
        for i, surface in enumerate(self.cached_surfaces):
348
            cr.set_source_surface(surface,
349
                                  self.rect.x + self._dx[i],
350
                                  self.rect.y + self._dy[i])
351
            cr.rectangle(self.rect.x + self._dx[i],
352
                         self.rect.y + self._dy[i],
353
                         self.rect.width,
354
                         self.rect.height)
355
            cr.fill()
356
357
        if len(self.labels) > 0:
358
            self.draw_label(cr)
359
360
    def hit(self, pos, readpixel=False):
361
        ''' Is (x, y) on top of the sprite? '''
362
        x, y = pos
363
        if x < self.rect.x:
364
            return False
365
        if x > self.rect.x + self.rect.width:
366
            return False
367
        if y < self.rect.y:
368
            return False
369
        if y > self.rect.y + self.rect.height:
370
            return False
371
        if readpixel:
372
            r, g, b, a = self.get_pixel(pos)
373
            if r == g == b == a == 0:
374
                return False
375
            if a == -1:
376
                return False
377
        return self._sprites.find_in_list(self)
378
379
    def draw_label(self, cr):
380
        ''' Draw the label based on its attributes '''
381
        # Create a pangocairo context
382
        cr = pangocairo.CairoContext(cr)
383
        my_width = self.rect.width - self._margins[0] - self._margins[2]
384
        if my_width < 0:
385
            my_width = 0
386
        my_height = self.rect.height - self._margins[1] - self._margins[3]
387
        for i in range(len(self.labels)):
388
            if hasattr(self, 'pl'):  # Reuse the pango layout
389
                pl = self.pl
390
            else:
391
                self.pl = cr.create_layout()
392
                pl = self.pl
393
            pl.set_text(str(self.labels[i]))
394
            self._fd.set_size(int(self._scale[i] * pango.SCALE))
395
            pl.set_font_description(self._fd)
396
            w = pl.get_size()[0] / pango.SCALE
397
            if w > my_width:
398
                if self._rescale[i]:
399
                    self._fd.set_size(
400
                        int(self._scale[i] * pango.SCALE * my_width / w))
401
                    pl.set_font_description(self._fd)
402
                    w = pl.get_size()[0] / pango.SCALE
403
                else:
404
                    pl.set_width(int(my_width * pango.SCALE))
405
                    pl.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
406
                    pl.set_text(str(self.labels[i]))
407
                    w = pl.get_size()[0] / pango.SCALE
408
            if self._x_pos[i] is not None:
409
                x = int(self.rect.x + self._x_pos[i])
410
            elif self._horiz_align[i] == 'center':
411
                x = int(self.rect.x + self._margins[0] + (my_width - w) / 2)
412
            elif self._horiz_align[i] == 'left':
413
                x = int(self.rect.x + self._margins[0])
414
            else:  # right
415
                x = int(self.rect.x + self.rect.width - w - self._margins[2])
416
            h = pl.get_size()[1] / pango.SCALE
417
            if self._y_pos[i] is not None:
418
                y = int(self.rect.y + self._y_pos[i])
419
            elif self._vert_align[i] == 'middle':
420
                y = int(self.rect.y + self._margins[1] + (my_height - h) / 2)
421
            elif self._vert_align[i] == 'top':
422
                y = int(self.rect.y + self._margins[1])
423
            else:  # bottom
424
                y = int(self.rect.y + self.rect.height - h - self._margins[3])
425
426
            cr.save()
427
            cr.translate(x, y)
428
            cr.set_source_rgb(self._color[0], self._color[1], self._color[2])
429
            cr.update_layout(pl)
430
            cr.show_layout(pl)
431
            cr.restore()
432
433
    def label_width(self):
434
        ''' Calculate the width of a label '''
435
        cr = pangocairo.CairoContext(self._sprites.cr)
436
        if cr is not None:
437
            max = 0
438
            for i in range(len(self.labels)):
439
                pl = cr.create_layout()
440
                pl.set_text(self.labels[i])
441
                self._fd.set_size(int(self._scale[i] * pango.SCALE))
442
                pl.set_font_description(self._fd)
443
                w = pl.get_size()[0] / pango.SCALE
444
                if w > max:
445
                    max = w
446
            return max
447
        else:
448
            return self.rect.width
449
450
    def label_safe_width(self):
451
        ''' Return maximum width for a label '''
452
        return self.rect.width - self._margins[0] - self._margins[2]
453
454
    def label_safe_height(self):
455
        ''' Return maximum height for a label '''
456
        return self.rect.height - self._margins[1] - self._margins[3]
457
458
    def label_left_top(self):
459
        ''' Return the upper-left corner of the label safe zone '''
460
        return(self._margins[0], self._margins[1])
461
462
    def get_pixel(self, pos, i=0):
463
        ''' Return the pixel at (x, y) '''
464
        x = int(pos[0] - self.rect.x)
465
        y = int(pos[1] - self.rect.y)
466
        if x < 0 or x > (self.rect.width - 1) or \
467
                y < 0 or y > (self.rect.height - 1):
468
            return(-1, -1, -1, -1)
469
        # Create a new 1x1 cairo surface.
470
        cs = cairo.ImageSurface(cairo.FORMAT_RGB24, 1, 1)
471
        cr = cairo.Context(cs)
472
        cr.set_source_surface(self.cached_surfaces[i], -x, -y)
473
        cr.rectangle(0, 0, 1, 1)
474
        cr.set_operator(cairo.OPERATOR_SOURCE)
475
        cr.fill()
476
        cs.flush()  # Ensure all the writing is done.
477
        pixels = cs.get_data()  # Read the pixel.
478
        return (ord(pixels[2]), ord(pixels[1]), ord(pixels[0]), 0)