~ubuntu-branches/ubuntu/trusty/sugar-sliderpuzzle-activity/trusty

« back to all changes in this revision

Viewing changes to SliderPuzzle.activity/SliderPuzzleWidget.py

  • Committer: Bazaar Package Importer
  • Author(s): Jani Monoses
  • Date: 2008-02-16 20:54:56 UTC
  • Revision ID: james.westby@ubuntu.com-20080216205456-yrap5ajfu0qyadf2
Tags: upstream-5
ImportĀ upstreamĀ versionĀ 5

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2007 World Wide Workshop Foundation
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 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 General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 
16
#
 
17
# If you find this activity useful or end up using parts of it in one of your
 
18
# own creations we would love to hear from you at info@WorldWideWorkshop.org !
 
19
#
 
20
 
 
21
import pygtk
 
22
pygtk.require('2.0')
 
23
import gtk, gobject
 
24
import pango
 
25
import md5
 
26
 
 
27
from mamamedia_modules import utils
 
28
 
 
29
#from utils import load_image, calculate_matrix, debug, SliderCreator, trace
 
30
 
 
31
from types import TupleType, ListType
 
32
from random import random
 
33
from time import time
 
34
from math import sqrt
 
35
from cStringIO import StringIO
 
36
import os
 
37
 
 
38
###
 
39
# General Information
 
40
###
 
41
 
 
42
up_key =    ['Up', 'KP_Up', 'KP_8']
 
43
down_key =  ['Down', 'KP_Down', 'KP_2']
 
44
left_key =  ['Left', 'KP_Left', 'KP_4']
 
45
right_key = ['Right', 'KP_Right', 'KP_6']
 
46
 
 
47
SLIDE_UP = 1
 
48
SLIDE_DOWN = 2
 
49
SLIDE_LEFT = 3
 
50
SLIDE_RIGHT = 4
 
51
 
 
52
def calculate_matrix (pieces):
 
53
    """ Given a number of pieces, calculate the best fit 2 dimensional matrix """
 
54
    rows = int(sqrt(pieces))
 
55
    cols = int(float(pieces) / rows)
 
56
    return rows*cols, rows, cols
 
57
 
 
58
 
 
59
class SliderCreator (gtk.gdk.Pixbuf):
 
60
    def __init__ (self, width, height, fname=None, tlist=None): #tlist):
 
61
        if width == -1:
 
62
            width = 564
 
63
        if height == -1:
 
64
            height = 564
 
65
        super(SliderCreator, self).__init__(gtk.gdk.COLORSPACE_RGB, False, 8, width, height)
 
66
        if tlist is None:
 
67
          items = []
 
68
          cmds = file(fname).readlines()
 
69
          if len(cmds) > 1:
 
70
              _x_ = eval(cmds[0])
 
71
              for i in range(16):
 
72
                  items.append(_x_)
 
73
                  _x_ = eval(cmds[1])
 
74
        else:
 
75
            items = tlist
 
76
        self.width = width
 
77
        self.height = height
 
78
        self.tlist = items
 
79
        self.prepare_stringed(2,2)
 
80
 
 
81
    #def scale_simple (self, w,h,m):
 
82
    #    return SliderCreator(w,h,tlist=self.tlist)
 
83
 
 
84
    #def subpixbuf (self, x,y,w,h):
 
85
    #    return SliderCreator(w,h,tlist=self.tlist)
 
86
 
 
87
    @classmethod
 
88
    def can_handle(klass, fname):
 
89
        return fname.lower().endswith('.sequence')
 
90
 
 
91
    def prepare_stringed (self, rows, cols):
 
92
        # We use a Pixmap as offscreen drawing canvas
 
93
        cm = gtk.gdk.colormap_get_system()
 
94
        pm = gtk.gdk.Pixmap(None, self.width, self.height, cm.get_visual().depth)
 
95
        #pangolayout = pm.create_pango_layout("")
 
96
        font_size = int(self.width / cols / 4)
 
97
        l = gtk.Label()
 
98
        pangolayout = pango.Layout(l.create_pango_context())
 
99
        pangolayout.set_font_description(pango.FontDescription("sans bold %i" % font_size))
 
100
        gc = pm.new_gc()
 
101
        gc.set_colormap(gtk.gdk.colormap_get_system())
 
102
        color = cm.alloc_color('white')
 
103
        gc.set_foreground(color)
 
104
        pm.draw_rectangle(gc, True, 0, 0, self.width, self.height)
 
105
        color = cm.alloc_color('black')
 
106
        gc.set_foreground(color)
 
107
 
 
108
        sw, sh = (self.width / cols), (self.height / rows)
 
109
        item = iter(self.tlist)
 
110
        for r in range(rows):
 
111
            for c in range(cols):
 
112
                px = sw * c
 
113
                py = sh * r
 
114
                #if c > 0 and r > 0:
 
115
                #    pm.draw_line(gc, px, 0, px, self.height-1)
 
116
                #    pm.draw_line(gc, 0, py, self.width-1, py)
 
117
                pangolayout.set_text(str(item.next()))
 
118
                pe = pangolayout.get_pixel_extents()
 
119
                pe = pe[1][2]/2, pe[1][3]/2
 
120
                pm.draw_layout(gc, px + (sw / 2) - pe[0],  py + (sh / 2) - pe[1], pangolayout)
 
121
        self.get_from_drawable(pm, cm, 0, 0, 0, 0, -1, -1)
 
122
 
 
123
utils.register_image_type(SliderCreator)
 
124
 
 
125
###
 
126
# Game Logic
 
127
###
 
128
 
 
129
class MatrixPosition (object):
 
130
    """ Helper class to hold a x/y coordinate, and move it by passing a direction,
 
131
    taking care of enforcing boundaries as needed.
 
132
    The x and y coords are 0 based. """
 
133
    def __init__ (self, rowsize, colsize, x=0, y=0):
 
134
        self.rowsize = rowsize
 
135
        self.colsize = colsize
 
136
        self.x = min(x, colsize-1)
 
137
        self.y = min(y, rowsize-1)
 
138
 
 
139
    def __eq__ (self, other):
 
140
        if isinstance(other, (TupleType, ListType)) and len(other) == 2:
 
141
            return self.x == other[0] and self.y == other[1]
 
142
        return False
 
143
 
 
144
    def __ne__ (self, other):
 
145
        return not self.__eq__ (other)
 
146
 
 
147
    def bottom_right (self):
 
148
        """ Move to the lower right position of the matrix, having 0,0 as the top left corner """
 
149
        self.x = self.colsize - 1
 
150
        self.y = self.rowsize-1
 
151
 
 
152
    def move (self, direction, count=1):
 
153
        """ Moving direction is actually the opposite of what is passed.
 
154
        We are moving the hole position, so if you slice a piece down into the hole,
 
155
        that hole is actually moving up.
 
156
        Returns bool, false if we can't move in the requested direction."""
 
157
        if direction == SLIDE_UP and self.y < self.rowsize-1:
 
158
            self.y += 1
 
159
            return True
 
160
        if direction == SLIDE_DOWN and self.y > 0:
 
161
            self.y -= 1
 
162
            return True
 
163
        if direction == SLIDE_LEFT and self.x < self.colsize-1:
 
164
            self.x += 1
 
165
            return True
 
166
        if direction == SLIDE_RIGHT and self.x > 0:
 
167
            self.x -= 1
 
168
            return True
 
169
        return False
 
170
 
 
171
    def clone (self):
 
172
        return MatrixPosition(self.rowsize, self.colsize, self.x, self.y)
 
173
 
 
174
    def _freeze (self):
 
175
        return (self.rowsize, self.colsize, self.x, self.y)
 
176
 
 
177
    def _thaw (self, obj):
 
178
        if obj is not None:
 
179
            self.rowsize, self.colsize, self.x, self.y = obj
 
180
        
 
181
 
 
182
class SliderPuzzleMap (object):
 
183
    """ This class holds the game logic.
 
184
    The current pieces position is held in self.pieces_map[YROW][XROW].
 
185
    """
 
186
    def __init__ (self, pieces=9, move_cb=None):
 
187
        self.reset(pieces)
 
188
        self.move_cb = move_cb
 
189
        self.solved = True
 
190
 
 
191
    def reset (self, pieces=9):
 
192
        self.pieces, self.rowsize, self.colsize = calculate_matrix(pieces)
 
193
        pieces_map = range(1,self.pieces+1)
 
194
        self.pieces_map = []
 
195
        for i in range(self.rowsize):
 
196
            self.pieces_map.append(pieces_map[i*self.colsize:(i+1)*self.colsize])
 
197
        self.hole_pos = MatrixPosition(self.rowsize, self.colsize)
 
198
        self.hole_pos.bottom_right()
 
199
        self.solved_map = [list(x) for x in self.pieces_map]
 
200
        self.solved_map[-1][-1] = None
 
201
 
 
202
    def randomize (self):
 
203
        """ To make sure the randomization is solvable, we don't simply shuffle the numbers.
 
204
        We move the hole in random directions through a finite number of iteractions. """
 
205
        # Remove the move callback temporarily
 
206
        cb = self.move_cb
 
207
        self.move_cb = None
 
208
 
 
209
        iteractions = self.rowsize * self.colsize * (int(100*random())+1)
 
210
 
 
211
        t = time()
 
212
        for i in range(iteractions):
 
213
            while not (self.do_move(int(4*random())+1)):
 
214
                pass
 
215
 
 
216
        t = time() - t
 
217
 
 
218
        # Now move the hole to the bottom right
 
219
        for x in range(self.colsize-self.hole_pos.x-1):
 
220
            self.do_move(SLIDE_LEFT)
 
221
        for y in range(self.rowsize-self.hole_pos.y-1):
 
222
            self.do_move(SLIDE_UP)
 
223
 
 
224
        # Put the callback where it was
 
225
        self.move_cb = cb
 
226
        self.solved = False
 
227
 
 
228
    def do_move (self, slide_direction):
 
229
        """
 
230
        The moves are relative to the moving piece:
 
231
        
 
232
        >>> jm = SliderPuzzleMap()
 
233
        >>> jm.debug_map()
 
234
        1 2 3
 
235
        4 5 6
 
236
        7 8 *
 
237
        >>> jm.do_move(SLIDE_DOWN)
 
238
        True
 
239
        >>> jm.debug_map() # DOWN
 
240
        1 2 3
 
241
        4 5 *
 
242
        7 8 6
 
243
        >>> jm.do_move(SLIDE_RIGHT)
 
244
        True
 
245
        >>> jm.debug_map() # RIGHT
 
246
        1 2 3
 
247
        4 * 5
 
248
        7 8 6
 
249
        >>> jm.do_move(SLIDE_UP)
 
250
        True
 
251
        >>> jm.debug_map() # UP
 
252
        1 2 3
 
253
        4 8 5
 
254
        7 * 6
 
255
        >>> jm.do_move(SLIDE_LEFT)
 
256
        True
 
257
        >>> jm.debug_map() # LEFT
 
258
        1 2 3
 
259
        4 8 5
 
260
        7 6 *
 
261
 
 
262
        We can't move over the matrix edges:
 
263
 
 
264
        >>> jm.do_move(SLIDE_LEFT)
 
265
        False
 
266
        >>> jm.debug_map() # LEFT
 
267
        1 2 3
 
268
        4 8 5
 
269
        7 6 *
 
270
        >>> jm.do_move(SLIDE_UP)
 
271
        False
 
272
        >>> jm.debug_map() # UP
 
273
        1 2 3
 
274
        4 8 5
 
275
        7 6 *
 
276
        >>> jm.do_move(SLIDE_RIGHT)
 
277
        True
 
278
        >>> jm.do_move(SLIDE_RIGHT)
 
279
        True
 
280
        >>> jm.do_move(SLIDE_RIGHT)
 
281
        False
 
282
        >>> jm.debug_map() # RIGHT x 3
 
283
        1 2 3
 
284
        4 8 5
 
285
        * 7 6
 
286
        >>> jm.do_move(SLIDE_DOWN)
 
287
        True
 
288
        >>> jm.do_move(SLIDE_DOWN)
 
289
        True
 
290
        >>> jm.do_move(SLIDE_DOWN)
 
291
        False
 
292
        >>> jm.debug_map() # DOWN x 3
 
293
        * 2 3
 
294
        1 8 5
 
295
        4 7 6
 
296
       """
 
297
        # What piece are we going to move?
 
298
        old_hole_pos = self.hole_pos.clone()
 
299
        if self.hole_pos.move(slide_direction):
 
300
            # Move was a success, now update the map
 
301
            self.pieces_map[old_hole_pos.y][old_hole_pos.x] = self.pieces_map[self.hole_pos.y][self.hole_pos.x]
 
302
            self.is_solved()
 
303
            if self.move_cb is not None:
 
304
                self.move_cb(self.hole_pos.x, self.hole_pos.y, old_hole_pos.x, old_hole_pos.y)
 
305
            return True
 
306
        return False
 
307
 
 
308
    def do_move_piece (self, piece):
 
309
        """ Move the piece (1 based index) into the hole, if possible
 
310
        >>> jm = SliderPuzzleMap()
 
311
        >>> jm.debug_map()
 
312
        1 2 3
 
313
        4 5 6
 
314
        7 8 *
 
315
        >>> jm.do_move_piece(6)
 
316
        True
 
317
        >>> jm.debug_map() # Moved 6
 
318
        1 2 3
 
319
        4 5 *
 
320
        7 8 6
 
321
        >>> jm.do_move_piece(2)
 
322
        False
 
323
        >>> jm.debug_map() # No move
 
324
        1 2 3
 
325
        4 5 *
 
326
        7 8 6
 
327
 
 
328
        Return True if a move was done, False otherwise.
 
329
        """
 
330
        for y in range(self.rowsize):
 
331
            for x in range(self.colsize):
 
332
                if self.pieces_map[y][x] == piece:
 
333
                    if self.hole_pos.x == x:
 
334
                        if abs(self.hole_pos.y-y) == 1:
 
335
                            return self.do_move(self.hole_pos.y > y and SLIDE_DOWN or SLIDE_UP)
 
336
                    elif self.hole_pos.y == y:
 
337
                        if abs(self.hole_pos.x-x) == 1:
 
338
                            return self.do_move(self.hole_pos.x > x and SLIDE_RIGHT or SLIDE_LEFT)
 
339
                    else:
 
340
                        return False
 
341
        return False
 
342
 
 
343
    def is_hole_at (self, x, y):
 
344
        """
 
345
        >>> jm = SliderPuzzleMap()
 
346
        >>> jm.debug_map()
 
347
        1 2 3
 
348
        4 5 6
 
349
        7 8 *
 
350
        >>> jm.is_hole_at(2,2)
 
351
        True
 
352
        >>> jm.is_hole_at(0,0)
 
353
        False
 
354
        """
 
355
        return self.hole_pos == (x,y)
 
356
 
 
357
    def is_solved (self):
 
358
        """
 
359
        >>> jm = SliderPuzzleMap()
 
360
        >>> jm.do_move_piece(6)
 
361
        True
 
362
        >>> jm.is_solved()
 
363
        False
 
364
        >>> jm.do_move_piece(6)
 
365
        True
 
366
        >>> jm.is_solved()
 
367
        True
 
368
        """
 
369
        if self.hole_pos != (self.colsize-1, self.rowsize-1):
 
370
            return False
 
371
        self.pieces_map[self.hole_pos.y][self.hole_pos.x] = None
 
372
        self.solved = self.pieces_map == self.solved_map
 
373
        return self.solved
 
374
        
 
375
        
 
376
 
 
377
    def get_cell_at (self, x, y):
 
378
        if x < 0 or x >= self.colsize or y < 0 or y >= self.rowsize or self.is_hole_at(x,y):
 
379
            return None
 
380
        return self.pieces_map[y][x]
 
381
 
 
382
    def debug_map (self):
 
383
        for y in range(self.rowsize):
 
384
            for x in range(self.colsize):
 
385
                if self.hole_pos == (x,y):
 
386
                    print "*",
 
387
                else:
 
388
                    print self.pieces_map[y][x],
 
389
            print
 
390
 
 
391
    def __call__ (self):
 
392
        self.debug_map()
 
393
 
 
394
    def _freeze (self):
 
395
        return {'pieces': self.pieces, 'rowsize': self.rowsize, 'colsize': self.colsize,
 
396
                'pieces_map': self.pieces_map, 'hole_pos_freeze': self.hole_pos._freeze()}
 
397
 
 
398
    def _thaw (self, obj):
 
399
        for k in obj.keys():
 
400
            if hasattr(self, k):
 
401
                setattr(self, k, obj[k])
 
402
        self.hole_pos._thaw(obj.get('hole_pos_freeze', None))
 
403
 
 
404
 
 
405
###
 
406
# Widget Definition
 
407
###
 
408
 
 
409
class SliderPuzzleWidget (gtk.Table):
 
410
    __gsignals__ = {'solved' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
 
411
                    'shuffled' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
 
412
                    'moved' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),}
 
413
    
 
414
    def __init__ (self, pieces=9, width=480, height=480):
 
415
        self.jumbler = SliderPuzzleMap(pieces, self.jumblermap_piece_move_cb)
 
416
        # We take this from the jumbler object because it may have altered our requested value
 
417
        gtk.Table.__init__(self, self.jumbler.rowsize, self.jumbler.colsize)
 
418
        self.image = None #gtk.Image()
 
419
        self.width = width
 
420
        self.height = height
 
421
        self.set_size_request(width, height)
 
422
        self.filename = None
 
423
 
 
424
    def prepare_pieces (self):
 
425
        """ set up a list of UI objects that will serve as pieces, ordered correctly """
 
426
        self.pieces = []
 
427
        if self.image is None:
 
428
        #    pb = self.image.get_pixbuf()
 
429
        #if self.image is None or pb is None:
 
430
            for i in range(self.jumbler.pieces):
 
431
                self.pieces.append(gtk.Button(str(i+1)))
 
432
                self.pieces[-1].connect("button-release-event", self.process_mouse_click, i+1)
 
433
                self.pieces[-1].show()
 
434
        else:
 
435
            if isinstance(self.image, SliderCreator):
 
436
                # ask for image creation
 
437
                self.image.prepare_stringed(self.jumbler.rowsize, self.jumbler.colsize)
 
438
        
 
439
            w = self.image.get_width() / self.jumbler.colsize
 
440
            h = self.image.get_height() / self.jumbler.rowsize
 
441
            for y in range(self.jumbler.rowsize):
 
442
                for x in range(self.jumbler.colsize):
 
443
                    img = gtk.Image()
 
444
                    img.set_from_pixbuf(self.image.subpixbuf(x*w, y*h, w-1, h-1))
 
445
                    img.show()
 
446
                    self.pieces.append(gtk.EventBox())
 
447
                    self.pieces[-1].add(img)
 
448
                    self.pieces[-1].connect("button-press-event", self.process_mouse_click, (y*self.jumbler.colsize)+x+1)
 
449
                    self.pieces[-1].show()
 
450
            self.set_row_spacings(1)
 
451
            self.set_col_spacings(1)
 
452
 
 
453
    @utils.trace
 
454
    def full_refresh (self):
 
455
        # Delete everything
 
456
        self.foreach(self.remove)
 
457
        self.prepare_pieces()
 
458
        # Add the pieces in their respective places
 
459
        for y in range(self.jumbler.rowsize):
 
460
            for x in range(self.jumbler.colsize):
 
461
                pos = self.jumbler.get_cell_at(x, y)
 
462
                if pos is not None:
 
463
                    self.attach(self.pieces[pos-1], x, x+1, y, y+1)
 
464
 
 
465
    def process_mouse_click (self, b, e, i):
 
466
        # i is the 1 based index of the piece
 
467
        self.jumbler.do_move_piece(i)
 
468
 
 
469
    def process_key (self, w, e):
 
470
        if self.get_parent() == None:
 
471
            return False
 
472
        k = gtk.gdk.keyval_name(e.keyval)
 
473
        if k in up_key:
 
474
            self.jumbler.do_move(SLIDE_UP)
 
475
            return True
 
476
        if k in down_key:
 
477
            self.jumbler.do_move(SLIDE_DOWN)
 
478
            return True
 
479
        if k in left_key:
 
480
            self.jumbler.do_move(SLIDE_LEFT)
 
481
            return True
 
482
        if k in right_key:
 
483
            self.jumbler.do_move(SLIDE_RIGHT)
 
484
            return True
 
485
        return False
 
486
 
 
487
    ### SliderPuzzleMap specific callbacks ###
 
488
 
 
489
    def jumblermap_piece_move_cb (self, hx, hy, px, py):
 
490
        if not hasattr(self, 'pieces'):
 
491
            return
 
492
        piece = self.pieces[self.jumbler.get_cell_at(px, py)-1]
 
493
        self.remove(piece)
 
494
        self.attach(piece, px, px+1, py, py+1)
 
495
        self.emit("moved")
 
496
        if self.jumbler.solved:
 
497
            self.emit("solved")
 
498
 
 
499
    ### Parent callable interface ###
 
500
 
 
501
    def get_nr_pieces (self):
 
502
        return self.jumbler.pieces
 
503
 
 
504
    @utils.trace
 
505
    def set_nr_pieces (self, nr_pieces):
 
506
        self.jumbler.reset(nr_pieces)
 
507
        self.resize(self.jumbler.rowsize, self.jumbler.colsize)
 
508
        self.randomize()
 
509
 
 
510
    @utils.trace
 
511
    def randomize (self):
 
512
        """ Jumble the SliderPuzzle """
 
513
        self.jumbler.randomize()
 
514
        self.full_refresh()
 
515
        self.emit("shuffled")
 
516
 
 
517
    @utils.trace
 
518
    def load_image (self, image, width=0, height=0):
 
519
        """ Loads an image from the file.
 
520
        width and height are processed as follows:
 
521
          -1 : follow the loaded image size
 
522
           0 : follow the size set on widget instantiation
 
523
           * : use that specific size"""
 
524
        if width == 0:
 
525
            width = self.width
 
526
        if height == 0:
 
527
            height = self.height
 
528
        if not isinstance(image, SliderCreator):
 
529
            self.image = utils.resize_image(image, width, height)
 
530
        else:
 
531
            self.image = image
 
532
        self.filename = True
 
533
        self.full_refresh()
 
534
 
 
535
    def set_image (self, image):
 
536
        # image is a pixbuf!
 
537
        self.image = image
 
538
        self.filename = True
 
539
 
 
540
    def set_image_from_str (self, image):
 
541
        fn = os.tempnam() 
 
542
        f = file(fn, 'w+b')
 
543
        f.write(image)
 
544
        f.close()
 
545
        i = gtk.Image()
 
546
        i.set_from_file(fn)
 
547
        os.remove(fn)
 
548
        self.image = i.get_pixbuf()
 
549
        self.filename = True
 
550
 
 
551
    def show_image (self):
 
552
        """ Shows the full image, used as visual clue for solved puzzle """
 
553
        # Delete everything
 
554
        self.foreach(self.remove)
 
555
        if hasattr(self, 'pieces'):
 
556
            del self.pieces
 
557
        # Resize to a single cell and use that for the image
 
558
        self.resize(1,1)
 
559
        img = gtk.Image()
 
560
        img.set_from_pixbuf(self.image)
 
561
        self.attach(img, 0,1,0,1)
 
562
        img.show()
 
563
 
 
564
    def get_image_as_png (self, cb=None):
 
565
        if self.image is None:
 
566
            return None
 
567
        rv = None
 
568
        if cb is None:
 
569
            rv = StringIO()
 
570
            cb = rv.write
 
571
        self.image.save_to_callback(cb, "png")
 
572
        if rv is not None:
 
573
            return rv.getvalue()
 
574
        else:
 
575
            return True
 
576
 
 
577
    def _freeze (self, journal=True):
 
578
        """ returns a json writable object representation capable of being used to restore our current status """
 
579
        if journal:
 
580
            return {'jumbler': self.jumbler._freeze(),
 
581
                    'image': self.get_image_as_png(),
 
582
                    }
 
583
        else:
 
584
            return {'jumbler': self.jumbler._freeze()}
 
585
 
 
586
    def _thaw (self, obj):
 
587
        """ retrieves a frozen status from a python object, as per _freeze """
 
588
        print obj['jumbler']
 
589
        self.jumbler._thaw(obj['jumbler'])
 
590
        if obj.has_key('image') and obj['image'] is not None:
 
591
            self.set_image_from_str(obj['image'])
 
592
            del obj['image']
 
593
        self.full_refresh()
 
594
 
 
595
def _test():
 
596
    import doctest
 
597
    doctest.testmod()
 
598
 
 
599
if __name__ == '__main__':
 
600
    _test()