Exemple #1
0
class Card:
    ''' Individual cards '''
    def __init__(self, scale=1.0):
        ''' Create the card and store its attributes '''
        self.spr = None
        self.index = None  # Calculated index
        self._scale = scale

    def create(self, string, attributes=None, sprites=None, file_path=None):
        if attributes is None:
            if self.spr is None:
                self.spr = Sprite(sprites, 0, 0, svg_str_to_pixbuf(string))
            else:
                self.spr.set_image(svg_str_to_pixbuf(string))
            self.index = None
        else:
            self.shape = attributes[0]
            self.color = attributes[1]
            self.num = attributes[2]
            self.fill = attributes[3]
            self.index = self.shape * COLORS * NUMBER * FILLS + \
                self.color * NUMBER * FILLS + \
                self.num * FILLS + \
                self.fill
            self.spr = Sprite(sprites, 0, 0, svg_str_to_pixbuf(string, True))
            if file_path is not None:
                self.spr.set_image(load_image(file_path, self._scale),
                                   i=1,
                                   dx=int(self._scale * CARD_WIDTH * .125),
                                   dy=int(self._scale * CARD_HEIGHT * .125))
        self.spr.set_label_attributes(self._scale * 24)
        self.spr.set_label('')

    def show_card(self, layer=2000):
        ''' Show the card '''
        if self.spr is not None:
            self.spr.set_layer(layer)
            self.spr.draw()

    def hide_card(self):
        ''' Hide a card '''
        if self.spr is not None:
            self.spr.hide()
Exemple #2
0
class Card:

    ''' Individual cards '''

    def __init__(self, scale=1.0):
        ''' Create the card and store its attributes '''
        self.spr = None
        self.index = None  # Calculated index
        self._scale = scale

    def create(self, string, attributes=None, sprites=None, file_path=None):
        if attributes is None:
            if self.spr is None:
                self.spr = Sprite(sprites, 0, 0, svg_str_to_pixbuf(string))
            else:
                self.spr.set_image(svg_str_to_pixbuf(string))
            self.index = None
        else:
            self.shape = attributes[0]
            self.color = attributes[1]
            self.num = attributes[2]
            self.fill = attributes[3]
            self.index = self.shape * COLORS * NUMBER * FILLS + \
                self.color * NUMBER * FILLS + \
                self.num * FILLS + \
                self.fill
            if self.spr is None:
                self.spr = Sprite(sprites, 0, 0, svg_str_to_pixbuf(string))
            else:
                self.spr.set_image(svg_str_to_pixbuf(string))

            if file_path is not None:
                self.spr.set_image(load_image(file_path, self._scale), i=1,
                                   dx=int(self._scale * CARD_WIDTH * .125),
                                   dy=int(self._scale * CARD_HEIGHT * .125))
        self.spr.set_label_attributes(self._scale * 24)
        self.spr.set_label('')

    def show_card(self, layer=2000):
        ''' Show the card '''
        if self.spr is not None:
            self.spr.set_layer(layer)
            self.spr.draw()

    def hide_card(self):
        ''' Hide a card '''
        if self.spr is not None:
            self.spr.hide()
Exemple #3
0
class Game():

    def __init__(self, canvas, parent=None, path=None, root=None, mode='array',
                 colors=['#A0FFA0', '#FF8080']):
        self._canvas = canvas
        self._parent = parent
        self._path = path
        self._root = root
        self._mode = mode
        self.current_image = 0
        self.playing = False
        self._timeout_id = None
        self._prev_mouse_pos = (0, 0)
        self._start_time = 0

        self._colors = ['#FFFFFF']
        self._colors.append(colors[0])
        self._colors.append(colors[1])

        self._canvas.add_events(
            Gdk.EventMask.BUTTON_PRESS_MASK |
            Gdk.EventMask.BUTTON_RELEASE_MASK |
            Gdk.EventMask.BUTTON_MOTION_MASK |
            Gdk.EventMask.POINTER_MOTION_MASK |
            Gdk.EventMask.POINTER_MOTION_HINT_MASK |
            Gdk.EventMask.TOUCH_MASK)

        self._canvas.connect('draw', self.__draw_cb)
        self._canvas.connect('event', self.__event_cb)

        self.configure(move=False)
        self.we_are_sharing = False

        self._start_time = 0
        self._timeout_id = None

        # Find the image files
        self._PATHS = glob.glob(os.path.join(self._path, 'images', '*.svg'))

        # Generate the sprites we'll need...
        self._sprites = Sprites(self._canvas)

        a = max(Gdk.Screen.width(), Gdk.Screen.height())
        b = min(Gdk.Screen.width(), Gdk.Screen.height())
        self._bg_pixbufs = []
        if self._parent.tablet_mode:  # text on top
            # landscape
            self._bg_pixbufs.append(svg_str_to_pixbuf(genhole(
                a, a,
                3 * style.GRID_CELL_SIZE,
                style.DEFAULT_SPACING,
                a - 3 * style.GRID_CELL_SIZE,
                style.GRID_CELL_SIZE * 3 + style.DEFAULT_SPACING)))
            # portrait
            self._bg_pixbufs.append(svg_str_to_pixbuf(genhole(
                a, a,
                3 * style.GRID_CELL_SIZE,
                style.DEFAULT_SPACING,
                b - 3 * style.GRID_CELL_SIZE,
                style.GRID_CELL_SIZE * 3 + style.DEFAULT_SPACING)))
        else:  # text on bottom
            # landscape
            self._bg_pixbufs.append(svg_str_to_pixbuf(genhole(
                a, a,
                3 * style.GRID_CELL_SIZE,
                b - style.GRID_CELL_SIZE * 4 - style.DEFAULT_SPACING,
                a - 3 * style.GRID_CELL_SIZE,
                b - style.GRID_CELL_SIZE - style.DEFAULT_SPACING)))
            # portrait
            self._bg_pixbufs.append(svg_str_to_pixbuf(genhole(
                a, a,
                3 * style.GRID_CELL_SIZE,
                a - style.GRID_CELL_SIZE * 4 - style.DEFAULT_SPACING,
                b - 3 * style.GRID_CELL_SIZE,
                a - style.GRID_CELL_SIZE - style.DEFAULT_SPACING)))

        if Gdk.Screen.width() > Gdk.Screen.height():
            self._bg = Sprite(self._sprites, 0, 0, self._bg_pixbufs[0])
        else:
            self._bg = Sprite(self._sprites, 0, 0, self._bg_pixbufs[1])
        self._bg.set_layer(-2)
        self._bg.type = 'background'

        size = 3 * self._dot_size + 4 * self._space
        x = int((Gdk.Screen.width() - size) / 2.)
        self._dots = []
        self._Dots = []  # larger dots for linear mode
        X = int((Gdk.Screen.width() - self._dot_size * 3) / 2.)
        Y = style.GRID_CELL_SIZE + self._yoff
        if self._parent.tablet_mode:
            yoffset = self._space * 2 + self._yoff
        else:
            yoffset = self._yoff
        for y in range(3):
            for x in range(3):
                xoffset = int((self._width - 3 * self._dot_size -
                               2 * self._space) / 2.)
                self._dots.append(
                    Sprite(self._sprites,
                           xoffset + x * (self._dot_size + self._space),
                           y * (self._dot_size + self._space) + yoffset,
                           self._new_dot_surface(color=self._colors[0])))
                self._dots[-1].type = -1  # No image
                self._dots[-1].set_label_attributes(72)
                self._dots[-1].set_label('?')

                self._Dots.append(
                    Sprite(
                        self._sprites, X, Y,
                        self._new_dot_surface(color=self._colors[0],
                                              large=True)))
                self._Dots[-1].type = -1  # No image
                self._Dots[-1].set_label_attributes(72 * 3)
                self._Dots[-1].set_label('?')

        self.number_of_images = len(self._PATHS)
        if USE_ART4APPS:
            self._art4apps = Art4Apps()
            self.number_of_images = len(self._art4apps.get_words())

        self._record_pixbufs = []
        for icon in ['media-audio', 'media-audio-recording']:
            self._record_pixbufs.append(
                GdkPixbuf.Pixbuf.new_from_file_at_size(
                    os.path.join(self._root, 'icons', icon + '.svg'),
                    style.GRID_CELL_SIZE, style.GRID_CELL_SIZE))

        self._play_pixbufs = []
        for icon in ['play-inactive', 'play']:
            self._play_pixbufs.append(
                GdkPixbuf.Pixbuf.new_from_file_at_size(
                    os.path.join(self._root, 'icons', icon + '.svg'),
                    style.GRID_CELL_SIZE, style.GRID_CELL_SIZE))

        self._speak_pixbufs = []
        for icon in ['speak-inactive', 'speak']:
            self._speak_pixbufs.append(
                GdkPixbuf.Pixbuf.new_from_file_at_size(
                    os.path.join(self._root, 'icons', icon + '.svg'),
                    style.GRID_CELL_SIZE, style.GRID_CELL_SIZE))

        left = style.GRID_CELL_SIZE
        right = Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE
        y0 = style.DEFAULT_SPACING + style.DEFAULT_PADDING
        y1 = y0 + style.GRID_CELL_SIZE
        y2 = y1 + style.GRID_CELL_SIZE
        if not self._parent.tablet_mode:
            dy = Gdk.Screen.height() - 4 * style.GRID_CELL_SIZE - \
                2 * style.DEFAULT_SPACING
            y0 += dy
            y1 += dy
            y2 += dy
        y3 = int((Gdk.Screen.height() - 2 * style.GRID_CELL_SIZE) / 2)

        self._record = Sprite(
            self._sprites, right, y0, self._record_pixbufs[RECORD_OFF])
        self._record.set_layer(1)
        self._record.type = 'record'

        self._play = Sprite(
            self._sprites, right, y1, self._play_pixbufs[PLAY_OFF])
        self._play.set_layer(1)
        self._play.type = 'play-inactive'

        self._speak = Sprite(
            self._sprites, right, y2, self._speak_pixbufs[SPEAK_OFF])
        self._speak.set_layer(1)
        self._speak.type = 'speak-inactive'

        self._next_prev_pixbufs = []
        for icon in ['go-previous', 'go-next', 'go-previous-inactive',
                     'go-next-inactive']:
            self._next_prev_pixbufs.append(
                GdkPixbuf.Pixbuf.new_from_file_at_size(
                    os.path.join(self._root, 'icons', icon + '.svg'),
                    style.GRID_CELL_SIZE, style.GRID_CELL_SIZE))

        self._prev = Sprite(
            self._sprites, left, y3, self._next_prev_pixbufs[PREV_INACTIVE])
        self._prev.set_layer(1)
        self._prev.type = 'prev'
        if self._mode == 'array':
            self._prev.hide()

        self._next = Sprite(
            self._sprites, right, y3, self._next_prev_pixbufs[NEXT])
        self._next.set_layer(1)
        self._next.type = 'next'
        if self._mode == 'array':
            self._next.hide()

    def configure(self, move=True):
        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - style.GRID_CELL_SIZE
        if not move:
            if self._height < self._width:
                self._scale = self._height / (3 * DOT_SIZE * 1.2)
            else:
                self._scale = self._width / (3 * DOT_SIZE * 1.2)
            self._scale /= 1.5
            self._dot_size = int(DOT_SIZE * self._scale)
            if self._parent.tablet_mode:  # text on top
                self._yoff = style.GRID_CELL_SIZE * 3 + style.DEFAULT_SPACING
            else:
                self._yoff = style.DEFAULT_SPACING
            self._space = int(self._dot_size / 5.)
            return

        left = style.GRID_CELL_SIZE
        right = Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE
        y0 = style.DEFAULT_SPACING + style.DEFAULT_PADDING
        y1 = y0 + style.GRID_CELL_SIZE
        y2 = y1 + style.GRID_CELL_SIZE
        if not self._parent.tablet_mode:
            dy = Gdk.Screen.height() - 4 * style.GRID_CELL_SIZE - \
                2 * style.DEFAULT_SPACING
            y0 += dy
            y1 += dy
            y2 += dy
        y3 = int((Gdk.Screen.height() - 2 * style.GRID_CELL_SIZE) / 2)
        self._record.move((right, y0))
        self._play.move((right, y1))
        self._speak.move((right, y2))
        self._prev.move((left, y3))
        self._next.move((right, y3))

        # Move the dots
        X = int((Gdk.Screen.width() - self._dot_size * 3) / 2.)
        Y = style.GRID_CELL_SIZE + self._yoff
        if self._parent.tablet_mode:
            yoffset = self._space * 2 + self._yoff
        else:
            yoffset = self._yoff
        for y in range(3):
            for x in range(3):
                xoffset = int((self._width - 3 * self._dot_size -
                               2 * self._space) / 2.)
                self._dots[x + y * 3].move(
                    (xoffset + x * (self._dot_size + self._space),
                        y * (self._dot_size + self._space) + yoffset))
                self._Dots[x + y * 3].move((X, Y))

        # switch orientation the bg sprite
        if Gdk.Screen.width() > Gdk.Screen.height():
            self._bg.set_image(self._bg_pixbufs[0])
        else:
            self._bg.set_image(self._bg_pixbufs[1])
        self._bg.set_layer(-2)

    def set_speak_icon_state(self, state):
        if state:
            self._speak.set_image(self._speak_pixbufs[SPEAK_ON])
            self._speak.type = 'speak'
        else:
            self._speak.set_image(self._speak_pixbufs[SPEAK_OFF])
            self._speak.type = 'speak-inactive'
        self._speak.set_layer(1)

    def set_record_icon_state(self, state):
        if state:
            self._record.set_image(self._record_pixbufs[RECORD_ON])
        else:
            self._record.set_image(self._record_pixbufs[RECORD_OFF])
        self._record.set_layer(1)

    def set_play_icon_state(self, state):
        if state:
            self._play.set_image(self._play_pixbufs[PLAY_ON])
            self._play.type = 'play'
        else:
            self._play.set_image(self._play_pixbufs[PLAY_OFF])
            self._play.type = 'play-inactive'
        self._play.set_layer(1)

    def autoplay(self):
        self.set_mode('linear')  # forces current image to 0
        self.playing = True
        self._autonext(next=False)

    def stop(self):
        self.playing = False
        if self._parent.audio_process is not None:
            self._parent.audio_process.terminate()
            self._parent.audio_process = None
        if self._timeout_id is not None:
            GObject.source_remove(self._timeout_id)
            self._timeout_id = None
        self._parent.autoplay_button.set_icon_name('media-playback-start')
        self._parent.autoplay_button.set_tooltip(_('Play'))
        self._parent.array_button.set_sensitive(True)

    def _autonext(self, next=True):
        self._timeout_id = None
        if not self.playing:
            return

        if next:
            self._Dots[self.current_image].hide()
            self.current_image += 1
            self._Dots[self.current_image].set_layer(100)
            if self.current_image == 8:
                self._next.set_image(
                    self._next_prev_pixbufs[NEXT_INACTIVE])
                self._next.set_layer(1)
            self._prev.set_image(self._next_prev_pixbufs[PREV])
            self._prev.set_layer(1)
        self._parent.check_audio_status()
        self._parent.check_text_status()
        GObject.idle_add(self._play_sound)

    def _poll_audio(self):
        if self._parent.audio_process is None:  # Already stopped?
            return

        if self._parent.audio_process.poll() is None:
            GObject.timeout_add(200, self._poll_audio)
        else:
            self._parent.audio_process = None
            self._next_image()

    def _play_sound(self):
        self._start_time = time.time()

        # Either play back a recording or speak the text
        if self._play.type == 'play':
            self._parent.playback_recording_cb()
            self._poll_audio()
        elif self._speak.type == 'speak':
            bounds = self._parent.text_buffer.get_bounds()
            text = self._parent.text_buffer.get_text(
                bounds[0], bounds[1], True)
            speak(text)
            self._next_image()

    def _next_image(self):
        accumulated_time = int(time.time() - self._start_time)
        if accumulated_time < 5:
            pause = 5 - accumulated_time
        else:
            pause = 1
        if self.playing and self.current_image < 8:
            self._timeout_id = GObject.timeout_add(pause * 1000,
                                                   self._autonext)
        else:
            self.stop()

    def __event_cb(self, win, event):
        ''' The mouse button was pressed. Is it on a sprite? or
            there was a gesture. '''

        left = right = False

        if event.type in (Gdk.EventType.TOUCH_BEGIN,
                          Gdk.EventType.TOUCH_CANCEL,
                          Gdk.EventType.TOUCH_END,
                          Gdk.EventType.BUTTON_PRESS,
                          Gdk.EventType.BUTTON_RELEASE):

            x = int(event.get_coords()[1])
            y = int(event.get_coords()[2])

            if event.type in (Gdk.EventType.TOUCH_BEGIN,
                              Gdk.EventType.BUTTON_PRESS):
                self._prev_mouse_pos = (x, y)
            elif event.type in (Gdk.EventType.TOUCH_END,
                                Gdk.EventType.BUTTON_RELEASE):

                if self._parent.audio_process is not None:
                    self._parent.audio_process.terminate()
                    self._parent.audio_process = None
                    terminated_audio = True
                else:
                    terminated_audio = False

                if self.playing:
                    self.stop()

                new_mouse_pos = (x, y)
                mouse_movement = (new_mouse_pos[0] - self._prev_mouse_pos[0],
                                  new_mouse_pos[1] - self._prev_mouse_pos[1])

                # horizontal gestures only
                if (abs(mouse_movement[0]) / 5) > abs(mouse_movement[1]):
                    if abs(mouse_movement[0]) > abs(mouse_movement[1]):
                        if mouse_movement[0] < 0:
                            right = True
                        else:
                            left = True

        if event.type in (Gdk.EventType.TOUCH_END,
                          Gdk.EventType.BUTTON_RELEASE):
            spr = self._sprites.find_sprite((x, y))
            if left or right or spr is not None:
                if spr.type in ['record', 'play', 'play-inactive', 'speak',
                                'speak-inactive']:
                    if spr.type == 'record':
                        self._parent.record_cb()
                    elif spr.type == 'play' and not terminated_audio:
                        self._parent.playback_recording_cb()
                    elif spr.type == 'speak':
                        bounds = self._parent.text_buffer.get_bounds()
                        text = self._parent.text_buffer.get_text(
                            bounds[0], bounds[1], True)
                        speak(text)
                    return
                elif self._mode == 'array':
                    return

                self._parent.speak_text_cb()

                if self._parent.recording:
                    self._parent.record_cb()

                if (left or spr.type == 'prev') and self.current_image > 0:
                    self._Dots[self.current_image].hide()
                    self.current_image -= 1
                    self._Dots[self.current_image].set_layer(100)
                    if self.current_image == 0:
                        self._prev.set_image(
                            self._next_prev_pixbufs[PREV_INACTIVE])
                    self._next.set_image(self._next_prev_pixbufs[NEXT])
                elif (right or spr.type == 'next') and self.current_image < 8:
                    self._Dots[self.current_image].hide()
                    self.current_image += 1
                    self._Dots[self.current_image].set_layer(100)
                    if self.current_image == 8:
                        self._next.set_image(
                            self._next_prev_pixbufs[NEXT_INACTIVE])
                    self._prev.set_image(self._next_prev_pixbufs[PREV])
                elif spr.type not in ['prev', 'background'] and \
                        self.current_image < 8:
                    self._Dots[self.current_image].hide()
                    self.current_image += 1
                    self._Dots[self.current_image].set_layer(100)
                    if self.current_image == 8:
                        self._next.set_image(
                            self._next_prev_pixbufs[NEXT_INACTIVE])
                    self._prev.set_image(self._next_prev_pixbufs[PREV])
                self._parent.check_audio_status()
                self._parent.check_text_status()
                self._prev.set_layer(1)
                self._next.set_layer(1)
        return False

    def get_mode(self):
        return self._mode

    def set_mode(self, mode):
        self.current_image = 0
        self._prev.set_image(self._next_prev_pixbufs[PREV_INACTIVE])
        self._next.set_image(self._next_prev_pixbufs[NEXT])
        if mode == 'array':
            self._mode = 'array'
            self._prev.hide()
            self._next.hide()
        else:
            self._mode = 'linear'
            self._prev.set_layer(1)
            self._next.set_layer(1)

        for i in range(9):
            if self._mode == 'array':
                self._dots[i].set_layer(100)
                self._Dots[i].hide()
            else:
                self._dots[i].hide()
                if self.current_image == i:
                    self._Dots[i].set_layer(100)
                else:
                    self._Dots[i].hide()

    def _all_clear(self):
        ''' Things to reinitialize when starting up a new game. '''
        if self._timeout_id is not None:
            GObject.source_remove(self._timeout_id)

        self.set_mode(self._mode)

        if self._mode == 'array':
            for dot in self._dots:
                if dot.type != -1:
                    dot.type = -1
                    dot.set_shape(self._new_dot_surface(
                        self._colors[abs(dot.type)]))
                    dot.set_label('?')
        else:
            for dot in self._Dots:
                if dot.type != -1:
                    dot.type = -1
                    dot.set_shape(self._new_dot_surface(
                        self._colors[abs(dot.type)],
                        large=True))
                    dot.set_label('?')
        self._dance_counter = 0
        self._dance_step()

    def _dance_step(self):
        ''' Short animation before loading new game '''
        if self._mode == 'array':
            for dot in self._dots:
                dot.set_shape(self._new_dot_surface(
                    self._colors[int(uniform(0, 3))]))
        else:
            self._Dots[0].set_shape(self._new_dot_surface(
                self._colors[int(uniform(0, 3))],
                large=True))

        self._dance_counter += 1
        if self._dance_counter < 10:
            self._timeout_id = GObject.timeout_add(500, self._dance_step)
        else:
            self._new_images()

    def new_game(self):
        ''' Start a new game. '''
        self._all_clear()

    def _new_images(self):
        ''' Select pictures at random '''
        used_images = [0] * self.number_of_images
        for i in range(9):
            random_selection = int(uniform(0, self.number_of_images))
            while used_images[random_selection] != 0:
                random_selection = int(uniform(0, self.number_of_images))
            used_images[random_selection] = 1
            self._dots[i].set_label('')
            self._dots[i].type = random_selection
            self._dots[i].set_shape(self._new_dot_surface(
                image=self._dots[i].type))

            self._Dots[i].set_label('')
            self._Dots[i].type = self._dots[i].type
            self._Dots[i].set_shape(self._new_dot_surface(
                image=self._Dots[i].type, large=True))

            if self._mode == 'array':
                self._dots[i].set_layer(100)
                self._Dots[i].hide()
            else:
                if self.current_image == i:
                    self._Dots[i].set_layer(100)
                else:
                    self._Dots[i].hide()
                self._dots[i].hide()

        if self.we_are_sharing:
            self._parent.send_new_images()

    def restore_game(self, dot_list):
        ''' Restore a game from the Journal or share '''

        self.set_mode(self._mode)

        for i, dot in enumerate(dot_list):
            self._dots[i].type = dot
            self._dots[i].set_shape(self._new_dot_surface(
                image=self._dots[i].type))
            self._dots[i].set_label('')

            self._Dots[i].type = dot
            self._Dots[i].set_shape(self._new_dot_surface(
                image=self._Dots[i].type, large=True))
            self._Dots[i].set_label('')

            if self._mode == 'array':
                self._dots[i].set_layer(100)
                self._Dots[i].hide()
            else:
                if self.current_image == i:
                    self._Dots[i].set_layer(100)
                else:
                    self._Dots[i].hide()
                self._dots[i].hide()

    def save_game(self):
        ''' Return dot list for saving to Journal or
        sharing '''
        dot_list = []
        for dot in self._dots:
            dot_list.append(dot.type)
        return dot_list

    def set_sharing(self, share=True):
        self.we_are_sharing = share

    def _grid_to_dot(self, pos):
        ''' calculate the dot index from a column and row in the grid '''
        return pos[0] + pos[1] * 3

    def _dot_to_grid(self, dot):
        ''' calculate the grid column and row for a dot '''
        return [dot % 3, int(dot / 3)]

    def __draw_cb(self, canvas, cr):
        self._sprites.redraw_sprites(cr=cr)

    def __expose_cb(self, win, event):
        ''' Callback to handle window expose events '''
        self.do_expose_event(event)
        return True

    # Handle the expose-event by drawing
    def do_expose_event(self, event):
        # Create the cairo context
        cr = self._canvas.window.cairo_create()

        # Restrict Cairo to the exposed area; avoid extra work
        cr.rectangle(event.area.x, event.area.y,
                     event.area.width, event.area.height)
        cr.clip()

        # Refresh sprite list
        if cr is not None:
            self._sprites.redraw_sprites(cr=cr)

    def _destroy_cb(self, win, event):
        Gtk.main_quit()

    def export(self):
        ''' Write dot to cairo surface. '''
        if self._mode == 'array':
            w = h = int(4 * self._space + 3 * self._dot_size)
            png_surface = cairo.ImageSurface(cairo.FORMAT_RGB24, w, h)
            cr = cairo.Context(png_surface)
            cr.set_source_rgb(192, 192, 192)
            cr.rectangle(0, 0, w, h)
            cr.fill()
            for i in range(9):
                y = self._space + int(i / 3.) * (self._dot_size + self._space)
                x = self._space + (i % 3) * (self._dot_size + self._space)
                cr.save()
                cr.set_source_surface(self._dots[i].images[0], x, y)
                cr.rectangle(x, y, self._dot_size, self._dot_size)
                cr.fill()
                cr.restore()
        else:
            w = h = int(2 * self._space + 3 * self._dot_size)
            png_surface = cairo.ImageSurface(cairo.FORMAT_RGB24, w, h)
            cr = cairo.Context(png_surface)
            cr.set_source_rgb(192, 192, 192)
            cr.rectangle(0, 0, w, h)
            cr.fill()
            y = self._space
            x = self._space
            cr.save()
            cr.set_source_surface(self._Dots[self.current_image].images[0],
                                  x, y)
            cr.rectangle(x, y, 3 * self._dot_size, 3 * self._dot_size)
            cr.fill()
            cr.restore()

        return png_surface

    def _new_dot_surface(self, color='#000000', image=None, large=False):
        ''' generate a dot of a color color '''

        if large:
            size = self._dot_size * 3
        else:
            size = self._dot_size
        self._svg_width = size
        self._svg_height = size

        if image is None:  # color dot
            self._stroke = color
            self._fill = color
            pixbuf = svg_str_to_pixbuf(
                self._header() +
                self._circle(size / 2., size / 2., size / 2.) +
                self._footer())
        else:
            if USE_ART4APPS:
                word = self._art4apps.get_words()[image]
                try:
                    pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                        self._art4apps.get_image_filename(word), size, size)
                except Exception, e:
                    _logger.error('new dot surface %s %s: %s' %
                                  (image, word, e))
                    word = 'zebra'  # default in case image is not found
                    pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                        self._art4apps.get_image_filename(word), size, size)
            else:
Exemple #4
0
class Game():
    ''' OLPC XO man color changer designed in memory of Nat Jacobson '''

    def __init__(self, canvas, parent=None, mycolors=['#A0FFA0', '#FF8080']):
        self._activity = parent
        self.colors = [mycolors[0]]
        self.colors.append(mycolors[1])

        self._canvas = canvas
        if parent is not None:
            parent.show_all()
            self._parent = parent

        self._canvas.connect("draw", self.__draw_cb)
        self._canvas.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        self._canvas.connect("button-press-event", self._button_press_cb)
        self._canvas.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK)
        self._canvas.connect('button-release-event', self._button_release_cb)
        self._canvas.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
        self._canvas.connect("motion-notify-event", self._mouse_move_cb)

        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - GRID_CELL_SIZE
        self._scale = self._width / 1200.

        self.press = None
        self.dragpos = [0, 0]
        self.startpos = [0, 0]

        self._dot_cache = {}
        self._xo_cache = {}

        self._radius = 22.5
        self._stroke_width = 9.5

        # Generate the sprites we'll need...
        self._sprites = Sprites(self._canvas)
        self._sprites.set_delay(True)
        self._dots = []
        self._xo_man = None
        self._generate_bg('#FFF')

        # First dot, starting angle
        self._cxy = [self._width / 2, self._height / 2]
        self._xy = [self._width / 2 + 120 * self._scale,
                    self._height / 2 - self._radius * self._scale]
        self._angle = 0
        self._dot_size_plus = self._radius * 3 * self._scale
        self._min = -self._dot_size_plus / 3
        self._max = self._height - (self._dot_size_plus / 2.2)

        self._zones = []
        self._calc_zones()
        self._generate_spiral()
        self._sprites.draw_all()

    def _calc_zones(self):
        for color in colors:
            rgb1 = _from_hex(color[0])
            rgb2 = _from_hex(color[1])
            dv = _contrast(rgb1, rgb2)
            dh = _delta_hue(rgb1, rgb2)
            self._zones.append(_zone(dv, dh))

    def _calc_next_dot_position(self):
        ''' calculate spiral coordinates '''
        dx = self._xy[0] - self._cxy[0]
        dy = self._xy[1] - self._cxy[1]
        r = sqrt(dx * dx + dy * dy)
        c = 2 * r * pi
        a = atan2(dy, dx)
        da = (self._dot_size_plus / c) * 2 * pi
        a += da
        r += self._dot_size_plus / (c / self._dot_size_plus)
        self._xy[0] = r * cos(a) + self._cxy[0]
        self._xy[1] = r * sin(a) + self._cxy[1]
        if self._xy[1] < self._min or self._xy[1] > self._max:
            self._calc_next_dot_position()

    def _generate_spiral(self):
        ''' Make a new set of dots for a sprial '''
        for z in range(4):
            for i in range(len(colors)):
                if self._zones[i] == z:
                    self._dots.append(
                        Sprite(self._sprites, self._xy[0], self._xy[1],
                               self._new_dot(colors[i])))
                    self._dots[-1].type = i
                    self._calc_next_dot_position()
        if self._xo_man is None:
            x = 510 * self._scale
            y = 280 * self._scale
            self._xo_man = Sprite(self._sprites, x, y,
                                 self._new_xo_man(self.colors))
            self._xo_man.type = None

    def move_dot(self, i, x, y):
        self._dots[i].move((x, y))

    def get_dot_xy(self, i):
        return self._dots[i].get_xy()

    def move_xo_man(self, x, y):
        self._xo_man.move((x, y))

    def get_xo_man_xy(self):
        return self._xo_man.get_xy()

    def rotate(self):
        x, y = self._dots[0].get_xy()
        for i in range(len(colors) - 1):
            self._dots[i].move(self._dots[i + 1].get_xy())
        self._dots[-1].move((x, y))

    def _generate_bg(self, color):
        ''' a background color '''
        self._bg = Sprite(self._sprites, 0, 0, self._new_background(color))
        self._bg.set_layer(0)
        self._bg.type = None

    def adj_background(self, color):
        ''' Change background '''
        self._bg.set_image(self._new_background(color))
        self._bg.set_layer(0)

    def _button_press_cb(self, win, event):
        win.grab_focus()
        x, y = map(int, event.get_coords())
        self.dragpos = [x, y]

        spr = self._sprites.find_sprite((x, y))
        if spr == None or spr == self._bg:
            return
        self.startpos = spr.get_xy()
        self.press = spr

    def _mouse_move_cb(self, win, event):
        """ Drag a rule with the mouse. """
        if self.press is None:
            self.dragpos = [0, 0]
            return True
        win.grab_focus()
        x, y = map(int, event.get_coords())
        dx = x - self.dragpos[0]
        dy = y - self.dragpos[1]
        self.press.move_relative((dx, dy))
        self.dragpos = [x, y]

    def _button_release_cb(self, win, event):
        if self.press == None:
            return True
        if _distance(self.press.get_xy(), self.startpos) < 20:
            if type(self.press.type) == int:
                self.i = self.press.type
                self._new_surface()
            self.press.move(self.startpos)
        self.press = None

    def _new_surface(self):
        self.colors[0] = colors[self.i][0]
        self.colors[1] = colors[self.i][1]
        self._xo_man.set_image(self._new_xo_man(colors[self.i]))
        self._xo_man.set_layer(100)

    def __draw_cb(self, canvas, cr):
        self._sprites.redraw_sprites(cr=cr)

    def do_expose_event(self, event):
        ''' Handle the expose-event by drawing '''
        # Restrict Cairo to the exposed area
        cr = self._canvas.window.cairo_create()
        cr.rectangle(event.area.x, event.area.y,
                event.area.width, event.area.height)
        cr.clip()
        # Refresh sprite list
        self._sprites.redraw_sprites(cr=cr)

    def _destroy_cb(self, win, event):
        Gtk.main_quit()

    def _new_dot(self, color):
        ''' generate a dot of a color color '''
        if True: # not color in self._dot_cache:
            self._stroke = color[0]
            self._fill = color[1]
            self._svg_width = int(60 * self._scale)
            self._svg_height = int(60 * self._scale)
            pixbuf = svg_str_to_pixbuf(
                self._header() + \
                '<circle cx="%f" cy="%f" r="%f" stroke="%s" fill="%s" \
stroke-width="%f" visibility="visible" />' % (
                        30 * self._scale, 30 * self._scale,
                        self._radius * self._scale, self._stroke,
                        self._fill, self._stroke_width * self._scale) + \
                self._footer())

            surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
                                         self._svg_width, self._svg_height)
            context = cairo.Context(surface)
            Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0)
            context.rectangle(0, 0, self._svg_width, self._svg_height)
            context.fill()
            # self._dot_cache[color] = surface

        return surface  # self._dot_cache[color]

    def _new_background(self, color):
        ''' Background color '''
        self._svg_width = int(self._width)
        self._svg_height = int(self._height)
        string = \
            self._header() + \
            '<rect width="%f" height="%f" x="%f" \
y="%f" fill="%s" stroke="none" visibility="visible" />' % (
                    self._width, self._height, 0, 0, color) + \
            self._footer()
        pixbuf = svg_str_to_pixbuf(string)
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
                                     self._svg_width, self._svg_height)
        context = cairo.Context(surface)
        Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0)
        context.rectangle(0, 0, self._svg_width, self._svg_height)
        context.fill()
        return surface

    def _new_xo_man(self, color):
        ''' generate a xo-man of a color color '''
        if True: # not color in self._xo_cache:
            self._stroke = color[0]
            self._fill = color[1]
            self._svg_width = int(240. * self._scale)
            self._svg_height = int(260. * self._scale)
            string = \
                self._header() + \
                '<g>' + \
                '<g id="XO">' + \
                '<path id="Line1" d="M%f,%f C%f,%f %f,%f %f,%f" stroke="%s" \
stroke-width="%f" stroke-linecap="round" fill="none" visibility="visible" />' \
% (
                        165.5 * self._scale, 97 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        74.5 * self._scale, 188 * self._scale,
                        self._stroke, 37 * self._scale) + \
                '<path id="Line2" d="M%f,%f C%f,%f %f,%f %f,%f" stroke="%s" \
stroke-width="%f" stroke-linecap="round" fill="none" visibility="visible" />' \
% (
                        165.5 * self._scale, 188 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        74.5 * self._scale, 97 * self._scale,
                        self._stroke, 37 * self._scale) + \
                '<path id="Fill1" d="M%f,%f C%f,%f %f,%f %f,%f" stroke="%s" \
stroke-width="%f" stroke-linecap="round" fill="none" visibility="visible" />' \
% (
                        165.5 * self._scale, 97 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        74.5 * self._scale, 188 * self._scale,
                        self._fill, 17 * self._scale) + \
                '<path id="Fill2" d="M%f,%f C%f,%f %f,%f %f,%f" stroke="%s" \
stroke-width="%f" stroke-linecap="round" fill="none" visibility="visible" />' \
% (
                        165.5 * self._scale, 188 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        74.5 * self._scale, 97 * self._scale,
                        self._fill, 17 * self._scale) + \
                '<circle id="Circle" cx="%f" cy="%f" r="%f" \
fill="%s" stroke="%s" stroke-width="%f" visibility="visible" />' % (
                        120 * self._scale, 61.5 * self._scale,
                        27.5 * self._scale,
                        self._fill, self._stroke, 11 * self._scale) + \
                '</g></g>' + \
                self._footer()
            pixbuf = svg_str_to_pixbuf(string)

            surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
                                         self._svg_width, self._svg_height)
            context = cairo.Context(surface)
            Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0)
            context.rectangle(0, 0, self._svg_width, self._svg_height)
            context.fill()
            # self._xo_cache[color] = surface
        return surface # self._xo_cache[color]

    def _header(self):
        return '<svg\n' + 'xmlns:svg="http:#www.w3.org/2000/svg"\n' + \
            'xmlns="http://www.w3.org/2000/svg"\n' + \
            'xmlns:xlink="http://www.w3.org/1999/xlink"\n' + \
            'version="1.1"\n' + 'width="' + str(self._svg_width) + '"\n' + \
            'height="' + str(self._svg_height) + '">\n'

    def _footer(self):
        return '</svg>\n'
Exemple #5
0
class Game():
    def __init__(self,
                 canvas,
                 parent=None,
                 path=None,
                 root=None,
                 mode='array',
                 colors=['#A0FFA0', '#FF8080']):
        self._canvas = canvas
        self._parent = parent
        self._path = path
        self._root = root
        self._mode = mode
        self.current_image = 0
        self.playing = False
        self._timeout_id = None
        self._prev_mouse_pos = (0, 0)
        self._start_time = 0

        self._colors = ['#FFFFFF']
        self._colors.append(colors[0])
        self._colors.append(colors[1])

        self._canvas.add_events(Gdk.EventMask.BUTTON_PRESS_MASK
                                | Gdk.EventMask.BUTTON_RELEASE_MASK
                                | Gdk.EventMask.BUTTON_MOTION_MASK
                                | Gdk.EventMask.POINTER_MOTION_MASK
                                | Gdk.EventMask.POINTER_MOTION_HINT_MASK
                                | Gdk.EventMask.TOUCH_MASK)

        self._canvas.connect('draw', self.__draw_cb)
        self._canvas.connect('event', self.__event_cb)

        self.configure(move=False)
        self.we_are_sharing = False

        self._start_time = 0
        self._timeout_id = None

        # Find the image files
        self._PATHS = glob.glob(os.path.join(self._path, 'images', '*.svg'))

        # Generate the sprites we'll need...
        self._sprites = Sprites(self._canvas)

        a = max(Gdk.Screen.width(), Gdk.Screen.height())
        b = min(Gdk.Screen.width(), Gdk.Screen.height())
        self._bg_pixbufs = []
        if self._parent.tablet_mode:  # text on top
            # landscape
            self._bg_pixbufs.append(
                svg_str_to_pixbuf(
                    genhole(a, a, 3 * style.GRID_CELL_SIZE,
                            style.DEFAULT_SPACING,
                            a - 3 * style.GRID_CELL_SIZE,
                            style.GRID_CELL_SIZE * 3 + style.DEFAULT_SPACING)))
            # portrait
            self._bg_pixbufs.append(
                svg_str_to_pixbuf(
                    genhole(a, a, 3 * style.GRID_CELL_SIZE,
                            style.DEFAULT_SPACING,
                            b - 3 * style.GRID_CELL_SIZE,
                            style.GRID_CELL_SIZE * 3 + style.DEFAULT_SPACING)))
        else:  # text on bottom
            # landscape
            self._bg_pixbufs.append(
                svg_str_to_pixbuf(
                    genhole(
                        a, a, 3 * style.GRID_CELL_SIZE,
                        b - style.GRID_CELL_SIZE * 4 - style.DEFAULT_SPACING,
                        a - 3 * style.GRID_CELL_SIZE,
                        b - style.GRID_CELL_SIZE - style.DEFAULT_SPACING)))
            # portrait
            self._bg_pixbufs.append(
                svg_str_to_pixbuf(
                    genhole(
                        a, a, 3 * style.GRID_CELL_SIZE,
                        a - style.GRID_CELL_SIZE * 4 - style.DEFAULT_SPACING,
                        b - 3 * style.GRID_CELL_SIZE,
                        a - style.GRID_CELL_SIZE - style.DEFAULT_SPACING)))

        if Gdk.Screen.width() > Gdk.Screen.height():
            self._bg = Sprite(self._sprites, 0, 0, self._bg_pixbufs[0])
        else:
            self._bg = Sprite(self._sprites, 0, 0, self._bg_pixbufs[1])
        self._bg.set_layer(-2)
        self._bg.type = 'background'

        size = 3 * self._dot_size + 4 * self._space
        x = int((Gdk.Screen.width() - size) / 2.)
        self._dots = []
        self._Dots = []  # larger dots for linear mode
        X = int((Gdk.Screen.width() - self._dot_size * 3) / 2.)
        Y = style.GRID_CELL_SIZE + self._yoff
        if self._parent.tablet_mode:
            yoffset = self._space * 2 + self._yoff
        else:
            yoffset = self._yoff
        for y in range(3):
            for x in range(3):
                xoffset = int(
                    (self._width - 3 * self._dot_size - 2 * self._space) / 2.)
                self._dots.append(
                    Sprite(self._sprites,
                           xoffset + x * (self._dot_size + self._space),
                           y * (self._dot_size + self._space) + yoffset,
                           self._new_dot_surface(color=self._colors[0])))
                self._dots[-1].type = -1  # No image
                self._dots[-1].set_label_attributes(72)
                self._dots[-1].set_label('?')

                self._Dots.append(
                    Sprite(
                        self._sprites, X, Y,
                        self._new_dot_surface(color=self._colors[0],
                                              large=True)))
                self._Dots[-1].type = -1  # No image
                self._Dots[-1].set_label_attributes(72 * 3)
                self._Dots[-1].set_label('?')

        self.number_of_images = len(self._PATHS)
        if USE_ART4APPS:
            self._art4apps = Art4Apps()
            self.number_of_images = len(self._art4apps.get_words())

        self._record_pixbufs = []
        for icon in ['media-audio', 'media-audio-recording']:
            self._record_pixbufs.append(
                GdkPixbuf.Pixbuf.new_from_file_at_size(
                    os.path.join(self._root, 'icons', icon + '.svg'),
                    style.GRID_CELL_SIZE, style.GRID_CELL_SIZE))

        self._play_pixbufs = []
        for icon in ['play-inactive', 'play']:
            self._play_pixbufs.append(
                GdkPixbuf.Pixbuf.new_from_file_at_size(
                    os.path.join(self._root, 'icons', icon + '.svg'),
                    style.GRID_CELL_SIZE, style.GRID_CELL_SIZE))

        self._speak_pixbufs = []
        for icon in ['speak-inactive', 'speak']:
            self._speak_pixbufs.append(
                GdkPixbuf.Pixbuf.new_from_file_at_size(
                    os.path.join(self._root, 'icons', icon + '.svg'),
                    style.GRID_CELL_SIZE, style.GRID_CELL_SIZE))

        left = style.GRID_CELL_SIZE
        right = Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE
        y0 = style.DEFAULT_SPACING + style.DEFAULT_PADDING
        y1 = y0 + style.GRID_CELL_SIZE
        y2 = y1 + style.GRID_CELL_SIZE
        if not self._parent.tablet_mode:
            dy = Gdk.Screen.height() - 4 * style.GRID_CELL_SIZE - \
                2 * style.DEFAULT_SPACING
            y0 += dy
            y1 += dy
            y2 += dy
        y3 = int((Gdk.Screen.height() - 2 * style.GRID_CELL_SIZE) / 2)

        self._record = Sprite(self._sprites, right, y0,
                              self._record_pixbufs[RECORD_OFF])
        self._record.set_layer(1)
        self._record.type = 'record'

        self._play = Sprite(self._sprites, right, y1,
                            self._play_pixbufs[PLAY_OFF])
        self._play.set_layer(1)
        self._play.type = 'play-inactive'

        self._speak = Sprite(self._sprites, right, y2,
                             self._speak_pixbufs[SPEAK_OFF])
        self._speak.set_layer(1)
        self._speak.type = 'speak-inactive'

        self._next_prev_pixbufs = []
        for icon in [
                'go-previous', 'go-next', 'go-previous-inactive',
                'go-next-inactive'
        ]:
            self._next_prev_pixbufs.append(
                GdkPixbuf.Pixbuf.new_from_file_at_size(
                    os.path.join(self._root, 'icons', icon + '.svg'),
                    style.GRID_CELL_SIZE, style.GRID_CELL_SIZE))

        self._prev = Sprite(self._sprites, left, y3,
                            self._next_prev_pixbufs[PREV_INACTIVE])
        self._prev.set_layer(1)
        self._prev.type = 'prev'
        if self._mode == 'array':
            self._prev.hide()

        self._next = Sprite(self._sprites, right, y3,
                            self._next_prev_pixbufs[NEXT])
        self._next.set_layer(1)
        self._next.type = 'next'
        if self._mode == 'array':
            self._next.hide()

    def configure(self, move=True):
        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - style.GRID_CELL_SIZE
        if not move:
            if self._height < self._width:
                self._scale = self._height / (3 * DOT_SIZE * 1.2)
            else:
                self._scale = self._width / (3 * DOT_SIZE * 1.2)
            self._scale /= 1.5
            self._dot_size = int(DOT_SIZE * self._scale)
            if self._parent.tablet_mode:  # text on top
                self._yoff = style.GRID_CELL_SIZE * 3 + style.DEFAULT_SPACING
            else:
                self._yoff = style.DEFAULT_SPACING
            self._space = int(self._dot_size / 5.)
            return

        left = style.GRID_CELL_SIZE
        right = Gdk.Screen.width() - 2 * style.GRID_CELL_SIZE
        y0 = style.DEFAULT_SPACING + style.DEFAULT_PADDING
        y1 = y0 + style.GRID_CELL_SIZE
        y2 = y1 + style.GRID_CELL_SIZE
        if not self._parent.tablet_mode:
            dy = Gdk.Screen.height() - 4 * style.GRID_CELL_SIZE - \
                2 * style.DEFAULT_SPACING
            y0 += dy
            y1 += dy
            y2 += dy
        y3 = int((Gdk.Screen.height() - 2 * style.GRID_CELL_SIZE) / 2)
        self._record.move((right, y0))
        self._play.move((right, y1))
        self._speak.move((right, y2))
        self._prev.move((left, y3))
        self._next.move((right, y3))

        # Move the dots
        X = int((Gdk.Screen.width() - self._dot_size * 3) / 2.)
        Y = style.GRID_CELL_SIZE + self._yoff
        if self._parent.tablet_mode:
            yoffset = self._space * 2 + self._yoff
        else:
            yoffset = self._yoff
        for y in range(3):
            for x in range(3):
                xoffset = int(
                    (self._width - 3 * self._dot_size - 2 * self._space) / 2.)
                self._dots[x + y * 3].move(
                    (xoffset + x * (self._dot_size + self._space),
                     y * (self._dot_size + self._space) + yoffset))
                self._Dots[x + y * 3].move((X, Y))

        # switch orientation the bg sprite
        if Gdk.Screen.width() > Gdk.Screen.height():
            self._bg.set_image(self._bg_pixbufs[0])
        else:
            self._bg.set_image(self._bg_pixbufs[1])
        self._bg.set_layer(-2)

    def set_speak_icon_state(self, state):
        if state:
            self._speak.set_image(self._speak_pixbufs[SPEAK_ON])
            self._speak.type = 'speak'
        else:
            self._speak.set_image(self._speak_pixbufs[SPEAK_OFF])
            self._speak.type = 'speak-inactive'
        self._speak.set_layer(1)

    def set_record_icon_state(self, state):
        if state:
            self._record.set_image(self._record_pixbufs[RECORD_ON])
        else:
            self._record.set_image(self._record_pixbufs[RECORD_OFF])
        self._record.set_layer(1)

    def set_play_icon_state(self, state):
        if state:
            self._play.set_image(self._play_pixbufs[PLAY_ON])
            self._play.type = 'play'
        else:
            self._play.set_image(self._play_pixbufs[PLAY_OFF])
            self._play.type = 'play-inactive'
        self._play.set_layer(1)

    def autoplay(self):
        self.set_mode('linear')  # forces current image to 0
        self.playing = True
        self._autonext(next=False)

    def stop(self):
        self.playing = False
        if self._parent.audio_process is not None:
            self._parent.audio_process.terminate()
            self._parent.audio_process = None
        if self._timeout_id is not None:
            GObject.source_remove(self._timeout_id)
            self._timeout_id = None
        self._parent.autoplay_button.set_icon_name('media-playback-start')
        self._parent.autoplay_button.set_tooltip(_('Play'))
        self._parent.array_button.set_sensitive(True)

    def _autonext(self, next=True):
        self._timeout_id = None
        if not self.playing:
            return

        if next:
            self._Dots[self.current_image].hide()
            self.current_image += 1
            self._Dots[self.current_image].set_layer(100)
            if self.current_image == 8:
                self._next.set_image(self._next_prev_pixbufs[NEXT_INACTIVE])
                self._next.set_layer(1)
            self._prev.set_image(self._next_prev_pixbufs[PREV])
            self._prev.set_layer(1)
        self._parent.check_audio_status()
        self._parent.check_text_status()
        GObject.idle_add(self._play_sound)

    def _poll_audio(self):
        if self._parent.audio_process is None:  # Already stopped?
            return

        if self._parent.audio_process.poll() is None:
            GObject.timeout_add(200, self._poll_audio)
        else:
            self._parent.audio_process = None
            self._next_image()

    def _play_sound(self):
        self._start_time = time.time()

        # Either play back a recording or speak the text
        if self._play.type == 'play':
            self._parent.playback_recording_cb()
            self._poll_audio()
        elif self._speak.type == 'speak':
            bounds = self._parent.text_buffer.get_bounds()
            text = self._parent.text_buffer.get_text(bounds[0], bounds[1],
                                                     True)
            speak(text)
            self._next_image()

    def _next_image(self):
        accumulated_time = int(time.time() - self._start_time)
        if accumulated_time < 5:
            pause = 5 - accumulated_time
        else:
            pause = 1
        if self.playing and self.current_image < 8:
            self._timeout_id = GObject.timeout_add(pause * 1000,
                                                   self._autonext)
        else:
            self.stop()

    def __event_cb(self, win, event):
        ''' The mouse button was pressed. Is it on a sprite? or
            there was a gesture. '''

        left = right = False

        if event.type in (Gdk.EventType.TOUCH_BEGIN,
                          Gdk.EventType.TOUCH_CANCEL, Gdk.EventType.TOUCH_END,
                          Gdk.EventType.BUTTON_PRESS,
                          Gdk.EventType.BUTTON_RELEASE):

            x = int(event.get_coords()[1])
            y = int(event.get_coords()[2])

            if event.type in (Gdk.EventType.TOUCH_BEGIN,
                              Gdk.EventType.BUTTON_PRESS):
                self._prev_mouse_pos = (x, y)
            elif event.type in (Gdk.EventType.TOUCH_END,
                                Gdk.EventType.BUTTON_RELEASE):

                if self._parent.audio_process is not None:
                    self._parent.audio_process.terminate()
                    self._parent.audio_process = None
                    terminated_audio = True
                else:
                    terminated_audio = False

                if self.playing:
                    self.stop()

                new_mouse_pos = (x, y)
                mouse_movement = (new_mouse_pos[0] - self._prev_mouse_pos[0],
                                  new_mouse_pos[1] - self._prev_mouse_pos[1])

                # horizontal gestures only
                if (abs(mouse_movement[0]) / 5) > abs(mouse_movement[1]):
                    if abs(mouse_movement[0]) > abs(mouse_movement[1]):
                        if mouse_movement[0] < 0:
                            right = True
                        else:
                            left = True

        if event.type in (Gdk.EventType.TOUCH_END,
                          Gdk.EventType.BUTTON_RELEASE):
            spr = self._sprites.find_sprite((x, y))
            if left or right or spr is not None:
                if spr.type in [
                        'record', 'play', 'play-inactive', 'speak',
                        'speak-inactive'
                ]:
                    if spr.type == 'record':
                        self._parent.record_cb()
                    elif spr.type == 'play' and not terminated_audio:
                        self._parent.playback_recording_cb()
                    elif spr.type == 'speak':
                        bounds = self._parent.text_buffer.get_bounds()
                        text = self._parent.text_buffer.get_text(
                            bounds[0], bounds[1], True)
                        speak(text)
                    return
                elif self._mode == 'array':
                    return

                self._parent.speak_text_cb()

                if self._parent.recording:
                    self._parent.record_cb()

                if (left or spr.type == 'prev') and self.current_image > 0:
                    self._Dots[self.current_image].hide()
                    self.current_image -= 1
                    self._Dots[self.current_image].set_layer(100)
                    if self.current_image == 0:
                        self._prev.set_image(
                            self._next_prev_pixbufs[PREV_INACTIVE])
                    self._next.set_image(self._next_prev_pixbufs[NEXT])
                elif (right or spr.type == 'next') and self.current_image < 8:
                    self._Dots[self.current_image].hide()
                    self.current_image += 1
                    self._Dots[self.current_image].set_layer(100)
                    if self.current_image == 8:
                        self._next.set_image(
                            self._next_prev_pixbufs[NEXT_INACTIVE])
                    self._prev.set_image(self._next_prev_pixbufs[PREV])
                elif spr.type not in ['prev', 'background'] and \
                        self.current_image < 8:
                    self._Dots[self.current_image].hide()
                    self.current_image += 1
                    self._Dots[self.current_image].set_layer(100)
                    if self.current_image == 8:
                        self._next.set_image(
                            self._next_prev_pixbufs[NEXT_INACTIVE])
                    self._prev.set_image(self._next_prev_pixbufs[PREV])
                self._parent.check_audio_status()
                self._parent.check_text_status()
                self._prev.set_layer(1)
                self._next.set_layer(1)
        return False

    def get_mode(self):
        return self._mode

    def set_mode(self, mode):
        self.current_image = 0
        self._prev.set_image(self._next_prev_pixbufs[PREV_INACTIVE])
        self._next.set_image(self._next_prev_pixbufs[NEXT])
        if mode == 'array':
            self._mode = 'array'
            self._prev.hide()
            self._next.hide()
        else:
            self._mode = 'linear'
            self._prev.set_layer(1)
            self._next.set_layer(1)

        for i in range(9):
            if self._mode == 'array':
                self._dots[i].set_layer(100)
                self._Dots[i].hide()
            else:
                self._dots[i].hide()
                if self.current_image == i:
                    self._Dots[i].set_layer(100)
                else:
                    self._Dots[i].hide()

    def _all_clear(self):
        ''' Things to reinitialize when starting up a new game. '''
        if self._timeout_id is not None:
            GObject.source_remove(self._timeout_id)

        self.set_mode(self._mode)

        if self._mode == 'array':
            for dot in self._dots:
                if dot.type != -1:
                    dot.type = -1
                    dot.set_shape(
                        self._new_dot_surface(self._colors[abs(dot.type)]))
                    dot.set_label('?')
        else:
            for dot in self._Dots:
                if dot.type != -1:
                    dot.type = -1
                    dot.set_shape(
                        self._new_dot_surface(self._colors[abs(dot.type)],
                                              large=True))
                    dot.set_label('?')
        self._dance_counter = 0
        self._dance_step()

    def _dance_step(self):
        ''' Short animation before loading new game '''
        if self._mode == 'array':
            for dot in self._dots:
                dot.set_shape(
                    self._new_dot_surface(self._colors[int(uniform(0, 3))]))
        else:
            self._Dots[0].set_shape(
                self._new_dot_surface(self._colors[int(uniform(0, 3))],
                                      large=True))

        self._dance_counter += 1
        if self._dance_counter < 10:
            self._timeout_id = GObject.timeout_add(500, self._dance_step)
        else:
            self._new_images()

    def new_game(self):
        ''' Start a new game. '''
        self._all_clear()

    def _new_images(self):
        ''' Select pictures at random '''
        used_images = [0] * self.number_of_images
        for i in range(9):
            random_selection = int(uniform(0, self.number_of_images))
            while used_images[random_selection] != 0:
                random_selection = int(uniform(0, self.number_of_images))
            used_images[random_selection] = 1
            self._dots[i].set_label('')
            self._dots[i].type = random_selection
            self._dots[i].set_shape(
                self._new_dot_surface(image=self._dots[i].type))

            self._Dots[i].set_label('')
            self._Dots[i].type = self._dots[i].type
            self._Dots[i].set_shape(
                self._new_dot_surface(image=self._Dots[i].type, large=True))

            if self._mode == 'array':
                self._dots[i].set_layer(100)
                self._Dots[i].hide()
            else:
                if self.current_image == i:
                    self._Dots[i].set_layer(100)
                else:
                    self._Dots[i].hide()
                self._dots[i].hide()

        if self.we_are_sharing:
            self._parent.send_new_images()

    def restore_game(self, dot_list):
        ''' Restore a game from the Journal or share '''

        self.set_mode(self._mode)

        for i, dot in enumerate(dot_list):
            self._dots[i].type = dot
            self._dots[i].set_shape(
                self._new_dot_surface(image=self._dots[i].type))
            self._dots[i].set_label('')

            self._Dots[i].type = dot
            self._Dots[i].set_shape(
                self._new_dot_surface(image=self._Dots[i].type, large=True))
            self._Dots[i].set_label('')

            if self._mode == 'array':
                self._dots[i].set_layer(100)
                self._Dots[i].hide()
            else:
                if self.current_image == i:
                    self._Dots[i].set_layer(100)
                else:
                    self._Dots[i].hide()
                self._dots[i].hide()

    def save_game(self):
        ''' Return dot list for saving to Journal or
        sharing '''
        dot_list = []
        for dot in self._dots:
            dot_list.append(dot.type)
        return dot_list

    def set_sharing(self, share=True):
        self.we_are_sharing = share

    def _grid_to_dot(self, pos):
        ''' calculate the dot index from a column and row in the grid '''
        return pos[0] + pos[1] * 3

    def _dot_to_grid(self, dot):
        ''' calculate the grid column and row for a dot '''
        return [dot % 3, int(dot / 3)]

    def __draw_cb(self, canvas, cr):
        self._sprites.redraw_sprites(cr=cr)

    def __expose_cb(self, win, event):
        ''' Callback to handle window expose events '''
        self.do_expose_event(event)
        return True

    # Handle the expose-event by drawing
    def do_expose_event(self, event):
        # Create the cairo context
        cr = self._canvas.window.cairo_create()

        # Restrict Cairo to the exposed area; avoid extra work
        cr.rectangle(event.area.x, event.area.y, event.area.width,
                     event.area.height)
        cr.clip()

        # Refresh sprite list
        if cr is not None:
            self._sprites.redraw_sprites(cr=cr)

    def _destroy_cb(self, win, event):
        Gtk.main_quit()

    def export(self):
        ''' Write dot to cairo surface. '''
        if self._mode == 'array':
            w = h = int(4 * self._space + 3 * self._dot_size)
            png_surface = cairo.ImageSurface(cairo.FORMAT_RGB24, w, h)
            cr = cairo.Context(png_surface)
            cr.set_source_rgb(192, 192, 192)
            cr.rectangle(0, 0, w, h)
            cr.fill()
            for i in range(9):
                y = self._space + int(i / 3.) * (self._dot_size + self._space)
                x = self._space + (i % 3) * (self._dot_size + self._space)
                cr.save()
                cr.set_source_surface(self._dots[i].images[0], x, y)
                cr.rectangle(x, y, self._dot_size, self._dot_size)
                cr.fill()
                cr.restore()
        else:
            w = h = int(2 * self._space + 3 * self._dot_size)
            png_surface = cairo.ImageSurface(cairo.FORMAT_RGB24, w, h)
            cr = cairo.Context(png_surface)
            cr.set_source_rgb(192, 192, 192)
            cr.rectangle(0, 0, w, h)
            cr.fill()
            y = self._space
            x = self._space
            cr.save()
            cr.set_source_surface(self._Dots[self.current_image].images[0], x,
                                  y)
            cr.rectangle(x, y, 3 * self._dot_size, 3 * self._dot_size)
            cr.fill()
            cr.restore()

        return png_surface

    def _new_dot_surface(self, color='#000000', image=None, large=False):
        ''' generate a dot of a color color '''

        if large:
            size = self._dot_size * 3
        else:
            size = self._dot_size
        self._svg_width = size
        self._svg_height = size

        if image is None:  # color dot
            self._stroke = color
            self._fill = color
            pixbuf = svg_str_to_pixbuf(self._header() +
                                       self._circle(size / 2., size /
                                                    2., size / 2.) +
                                       self._footer())
        else:
            if USE_ART4APPS:
                word = self._art4apps.get_words()[image]
                try:
                    pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                        self._art4apps.get_image_filename(word), size, size)
                except Exception, e:
                    _logger.error('new dot surface %s %s: %s' %
                                  (image, word, e))
                    word = 'zebra'  # default in case image is not found
                    pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                        self._art4apps.get_image_filename(word), size, size)
            else:
class BBoardActivity(activity.Activity):
    ''' Make a slideshow from starred Journal entries. '''

    def __init__(self, handle):
        ''' Initialize the toolbars and the work surface '''
        super(BBoardActivity, self).__init__(handle)

        self.datapath = get_path(activity, 'instance')

        self._hw = get_hardware()

        self._playback_buttons = {}
        self._audio_recordings = {}
        self.colors = profile.get_color().to_string().split(',')

        self._setup_toolbars()
        self._setup_canvas()

        self.slides = []
        self._setup_workspace()

        self._buddies = [profile.get_nick_name()]
        self._setup_presence_service()

        self._thumbs = []
        self._thumbnail_mode = False

        self._recording = False
        self._grecord = None
        self._alert = None

        self._dirty = False

    def _setup_canvas(self):
        ''' Create a canvas '''
        self._canvas = gtk.DrawingArea()
        self._canvas.set_size_request(int(gtk.gdk.screen_width()),
                                      int(gtk.gdk.screen_height()))
        self._canvas.show()
        self.set_canvas(self._canvas)
        self.show_all()

        self._canvas.set_flags(gtk.CAN_FOCUS)
        self._canvas.add_events(gtk.gdk.BUTTON_PRESS_MASK)
        self._canvas.add_events(gtk.gdk.POINTER_MOTION_MASK)
        self._canvas.add_events(gtk.gdk.BUTTON_RELEASE_MASK)
        self._canvas.add_events(gtk.gdk.KEY_PRESS_MASK)
        self._canvas.connect("expose-event", self._expose_cb)
        self._canvas.connect("button-press-event", self._button_press_cb)
        self._canvas.connect("button-release-event", self._button_release_cb)
        self._canvas.connect("motion-notify-event", self._mouse_move_cb)

    def _setup_workspace(self):
        ''' Prepare to render the datastore entries. '''

        # Use the lighter color for the text background
        if lighter_color(self.colors) == 0:
            tmp = self.colors[0]
            self.colors[0] = self.colors[1]
            self.colors[1] = tmp

        self._width = gtk.gdk.screen_width()
        self._height = gtk.gdk.screen_height()
        self._scale = gtk.gdk.screen_height() / 900.

        if not HAVE_TOOLBOX and self._hw[0:2] == 'xo':
            titlef = 18
            descriptionf = 12
        else:
            titlef = 36
            descriptionf = 24

        self._find_starred()
        for ds in self.dsobjects:
            if 'title' in ds.metadata:
                title = ds.metadata['title']
            else:
                title = None
            pixbuf = None
            media_object = False
            mimetype = None
            if 'mime_type' in ds.metadata:
                mimetype = ds.metadata['mime_type']
            if mimetype[0:5] == 'image':
                pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(
                    ds.file_path, MAXX, MAXY)
                    # ds.file_path, 300, 225)
                media_object = True
            else:
                pixbuf = get_pixbuf_from_journal(ds, MAXX, MAXY)  # 300, 225)
            if 'description' in ds.metadata:
                desc = ds.metadata['description']
            else:
                desc = None
            self.slides.append(Slide(True, ds.object_id, self.colors,
                                     title, pixbuf, desc))

        # Generate the sprites we'll need...
        self._sprites = Sprites(self._canvas)

        self._help = Sprite(
            self._sprites,
            int((self._width - int(PREVIEWW * self._scale)) / 2),
            int(PREVIEWY * self._scale),
            gtk.gdk.pixbuf_new_from_file_at_size(
                os.path.join(activity.get_bundle_path(), 'help.png'),
                int(PREVIEWW * self._scale), int(PREVIEWH * self._scale)))
        self._help.hide()

        self._genblanks(self.colors)

        self._title = Sprite(self._sprites, 0, 0, self._title_pixbuf)
        self._title.set_label_attributes(int(titlef * self._scale),
                                         rescale=False)
        self._preview = Sprite(self._sprites,
            int((self._width - int(PREVIEWW * self._scale)) / 2),
            int(PREVIEWY * self._scale), self._preview_pixbuf)

        self._description = Sprite(self._sprites,
                                   int(DESCRIPTIONX * self._scale),
                                   int(DESCRIPTIONY * self._scale),
                                   self._desc_pixbuf)
        self._description.set_label_attributes(int(descriptionf * self._scale))

        self._my_canvas = Sprite(self._sprites, 0, 0, self._canvas_pixbuf)
        self._my_canvas.set_layer(BOTTOM)

        self._clear_screen()

        self.i = 0
        self._show_slide()

        self._playing = False
        self._rate = 10

    def _genblanks(self, colors):
        ''' Need to cache these '''
        self._title_pixbuf = svg_str_to_pixbuf(
            genblank(self._width, int(TITLEH * self._scale), colors))
        self._preview_pixbuf = svg_str_to_pixbuf(
            genblank(int(PREVIEWW * self._scale), int(PREVIEWH * self._scale),
                     colors))
        self._desc_pixbuf = svg_str_to_pixbuf(
            genblank(int(self._width - (2 * DESCRIPTIONX * self._scale)),
                     int(DESCRIPTIONH * self._scale), colors))
        self._canvas_pixbuf = svg_str_to_pixbuf(
            genblank(self._width, self._height, (colors[0], colors[0])))

    def _setup_toolbars(self):
        ''' Setup the toolbars. '''

        self.max_participants = 6

        if HAVE_TOOLBOX:
            toolbox = ToolbarBox()

            # Activity toolbar
            activity_button_toolbar = ActivityToolbarButton(self)

            toolbox.toolbar.insert(activity_button_toolbar, 0)
            activity_button_toolbar.show()

            self.set_toolbar_box(toolbox)
            toolbox.show()
            self.toolbar = toolbox.toolbar

            self.record_toolbar = gtk.Toolbar()
            record_toolbar_button = ToolbarButton(
                label=_('Record a sound'),
                page=self.record_toolbar,
                icon_name='media-audio')
            self.record_toolbar.show_all()
            record_toolbar_button.show()
            toolbox.toolbar.insert(record_toolbar_button, -1)
        else:
            # Use pre-0.86 toolbar design
            primary_toolbar = gtk.Toolbar()
            toolbox = activity.ActivityToolbox(self)
            self.set_toolbox(toolbox)
            toolbox.add_toolbar(_('Page'), primary_toolbar)
            self.record_toolbar = gtk.Toolbar()
            toolbox.add_toolbar(_('Record'), self.record_toolbar)
            toolbox.show()
            toolbox.set_current_toolbar(1)
            self.toolbar = primary_toolbar

        self._prev_button = button_factory(
            'go-previous-inactive', self.toolbar, self._prev_cb,
            tooltip=_('Prev slide'), accelerator='<Ctrl>P')

        self._next_button = button_factory(
            'go-next', self.toolbar, self._next_cb,
            tooltip=_('Next slide'), accelerator='<Ctrl>N')


        separator_factory(self.toolbar)

        slide_button = radio_factory('slide-view', self.toolbar,
                                     self._slides_cb, group=None,
                                     tooltip=_('Slide view'))
        radio_factory('thumbs-view', self.toolbar, self._thumbs_cb,
                      tooltip=_('Thumbnail view'),
                      group=slide_button)

        button_factory('view-fullscreen', self.toolbar,
                       self.do_fullscreen_cb, tooltip=_('Fullscreen'),
                       accelerator='<Alt>Return')

        separator_factory(self.toolbar)

        journal_button = button_factory(
            'write-journal', self.toolbar, self._do_journal_cb,
            tooltip=_('Update description'))
        self._palette = journal_button.get_palette()
        msg_box = gtk.HBox()

        sw = gtk.ScrolledWindow()
        sw.set_size_request(int(gtk.gdk.screen_width() / 2),
                            2 * style.GRID_CELL_SIZE)
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        self._text_view = gtk.TextView()
        self._text_view.set_left_margin(style.DEFAULT_PADDING)
        self._text_view.set_right_margin(style.DEFAULT_PADDING)
        self._text_view.set_wrap_mode(gtk.WRAP_WORD_CHAR)
        self._text_view.connect('focus-out-event',
                               self._text_view_focus_out_event_cb)
        sw.add(self._text_view)
        sw.show()
        msg_box.pack_start(sw, expand=False)
        msg_box.show_all()

        self._palette.set_content(msg_box)

        label_factory(self.record_toolbar, _('Record a sound') + ':')
        self._record_button = button_factory(
            'media-record', self.record_toolbar,
            self._record_cb, tooltip=_('Start recording'))

        separator_factory(self.record_toolbar)

        # Look to see if we have audio previously recorded
        obj_id = self._get_audio_obj_id()
        dsobject = self._search_for_audio_note(obj_id)
        if dsobject is not None:
            _logger.debug('Found previously recorded audio')
            self._add_playback_button(profile.get_nick_name(),
                                      self.colors,
                                      dsobject.file_path)

        if HAVE_TOOLBOX:
            button_factory('system-restart', activity_button_toolbar,
                           self._resend_cb, tooltip=_('Refresh'))
            separator_factory(activity_button_toolbar)
            self._save_pdf = button_factory(
                'save-as-pdf', activity_button_toolbar,
                self._save_as_pdf_cb, tooltip=_('Save as PDF'))
        else:
            separator_factory(self.toolbar)
            self._save_pdf = button_factory(
                'save-as-pdf', self.toolbar,
                self._save_as_pdf_cb, tooltip=_('Save as PDF'))

        if HAVE_TOOLBOX:
            separator_factory(toolbox.toolbar, True, False)

            stop_button = StopButton(self)
            stop_button.props.accelerator = '<Ctrl>q'
            toolbox.toolbar.insert(stop_button, -1)
            stop_button.show()

    def _do_journal_cb(self, button):
        self._dirty = True
        if self._palette:
            if not self._palette.is_up():
                self._palette.popup(immediate=True,
                                    state=self._palette.SECONDARY)
            else:
                self._palette.popdown(immediate=True)
            return

    def _text_view_focus_out_event_cb(self, widget, event):
        buffer = self._text_view.get_buffer()
        start_iter = buffer.get_start_iter()
        end_iter = buffer.get_end_iter()
        self.slides[self.i].desc = buffer.get_text(start_iter, end_iter)
        self._show_slide()

    def _destroy_cb(self, win, event):
        ''' Clean up on the way out. '''
        gtk.main_quit()

    def _find_starred(self):
        ''' Find all the favorites in the Journal. '''
        self.dsobjects, nobjects = datastore.find({'keep': '1'})
        _logger.debug('found %d starred items', nobjects)

    def _prev_cb(self, button=None):
        ''' The previous button has been clicked; goto previous slide. '''
        if self.i > 0:
            self.i -= 1
            self._show_slide(direction=-1)

    def _next_cb(self, button=None):
        ''' The next button has been clicked; goto next slide. '''
        if self.i < len(self.slides) - 1:
            self.i += 1
            self._show_slide()

    def _save_as_pdf_cb(self, button=None):
        ''' Export an PDF version of the slideshow to the Journal. '''
        _logger.debug('saving to PDF...')
        if 'description' in self.metadata:
            tmp_file = save_pdf(self, self._buddies,
                                description=self.metadata['description'])
        else:
            tmp_file = save_pdf(self, self._buddies)
        _logger.debug('copying PDF file to Journal...')
        dsobject = datastore.create()
        dsobject.metadata['title'] = profile.get_nick_name() + ' ' + \
                                     _('Bboard')
        dsobject.metadata['icon-color'] = profile.get_color().to_string()
        dsobject.metadata['mime_type'] = 'application/pdf'
        dsobject.set_file_path(tmp_file)
        dsobject.metadata['activity'] = 'org.laptop.sugar.ReadActivity'
        datastore.write(dsobject)
        dsobject.destroy()
        return

    def _clear_screen(self):
        ''' Clear the screen to the darker of the two user colors. '''
        self._title.hide()
        self._preview.hide()
        self._description.hide()
        if hasattr(self, '_thumbs'):
            for thumbnail in self._thumbs:
                thumbnail[0].hide()
        self.invalt(0, 0, self._width, self._height)

        # Reset drag settings
        self._press = None
        self._release = None
        self._dragpos = [0, 0]
        self._total_drag = [0, 0]
        self.last_spr_moved = None

    def _update_colors(self):
        ''' Match the colors to those of the slide originator. '''
        if len(self.slides) == 0:
            return
        self._genblanks(self.slides[self.i].colors)
        self._title.set_image(self._title_pixbuf)
        self._preview.set_image(self._preview_pixbuf)
        self._description.set_image(self._desc_pixbuf)
        self._my_canvas.set_image(self._canvas_pixbuf)

    def _show_slide(self, direction=1):
        ''' Display a title, preview image, and decription for slide. '''
        self._clear_screen()
        self._update_colors()

        if len(self.slides) == 0:
            self._prev_button.set_icon('go-previous-inactive')
            self._next_button.set_icon('go-next-inactive')
            self._description.set_label(
                _('Do you have any items in your Journal starred?'))
            self._help.set_layer(TOP)
            self._description.set_layer(MIDDLE)
            return

        if self.i == 0:
            self._prev_button.set_icon('go-previous-inactive')
        else:
            self._prev_button.set_icon('go-previous')
        if self.i == len(self.slides) - 1:
            self._next_button.set_icon('go-next-inactive')
        else:
            self._next_button.set_icon('go-next')

        pixbuf = self.slides[self.i].pixbuf
        if pixbuf is not None:
            self._preview.set_shape(pixbuf.scale_simple(
                    int(PREVIEWW * self._scale),
                    int(PREVIEWH * self._scale),
                    gtk.gdk.INTERP_NEAREST))
            self._preview.set_layer(MIDDLE)
        else:
            if self._preview is not None:
                self._preview.hide()

        self._title.set_label(self.slides[self.i].title)
        self._title.set_layer(MIDDLE)

        if self.slides[self.i].desc is not None:
            self._description.set_label(self.slides[self.i].desc)
            self._description.set_layer(MIDDLE)
            text_buffer = gtk.TextBuffer()
            text_buffer.set_text(self.slides[self.i].desc)
            self._text_view.set_buffer(text_buffer)
        else:
            self._description.set_label('')
            self._description.hide()

    def _add_playback_button(self, nick, colors, audio_file):
        ''' Add a toolbar button for this audio recording '''
        if nick not in self._playback_buttons:
            self._playback_buttons[nick] = button_factory(
                'xo-chat',  self.record_toolbar,
                self._playback_recording_cb, cb_arg=nick,
                tooltip=_('Audio recording by %s') % (nick))
            xocolor = XoColor('%s,%s' % (colors[0], colors[1]))
            icon = Icon(icon_name='xo-chat', xo_color=xocolor)
            icon.show()
            self._playback_buttons[nick].set_icon_widget(icon)
            self._playback_buttons[nick].show()
        self._audio_recordings[nick] = audio_file

    def _slides_cb(self, button=None):
        if self._thumbnail_mode:
            self._thumbnail_mode = False
            self.i = self._current_slide
            self._show_slide()

    def _thumbs_cb(self, button=None):
        ''' Toggle between thumbnail view and slideshow view. '''
        if not self._thumbnail_mode:
            self._current_slide = self.i
            self._thumbnail_mode = True
            self._clear_screen()

            self._prev_button.set_icon('go-previous-inactive')
            self._next_button.set_icon('go-next-inactive')

            n = int(ceil(sqrt(len(self.slides))))
            if n > 0:
                w = int(self._width / n)
            else:
                w = self._width
            h = int(w * 0.75)  # maintain 4:3 aspect ratio
            x_off = int((self._width - n * w) / 2)
            x = x_off
            y = 0
            self._thumbs = []
            for i in range(len(self.slides)):
                self._show_thumb(i, x, y, w, h)
                x += w
                if x + w > self._width:
                    x = x_off
                    y += h
            self.i = 0  # Reset position in slideshow to the beginning
        return False

    def _show_thumb(self, i, x, y, w, h):
        ''' Display a preview image and title as a thumbnail. '''
        pixbuf = self.slides[i].pixbuf
        if pixbuf is not None:
            pixbuf_thumb = pixbuf.scale_simple(
                int(w), int(h), gtk.gdk.INTERP_TILES)
        else:
            pixbuf_thumb = svg_str_to_pixbuf(
                genblank(int(w), int(h), self.slides[i].colors))
        # Create a Sprite for this thumbnail
        self._thumbs.append([Sprite(self._sprites, x, y, pixbuf_thumb),
                             x, y, i])
        self._thumbs[i][0].set_image(
            svg_str_to_pixbuf(svg_rectangle(int(w), int(h),
                                            self.slides[i].colors)), i=1)
        self._thumbs[i][0].set_layer(TOP)

    def _expose_cb(self, win, event):
        ''' Callback to handle window expose events '''
        self.do_expose_event(event)
        return True

    # Handle the expose-event by drawing
    def do_expose_event(self, event):

        # Create the cairo context
        cr = self.canvas.window.cairo_create()

        # Restrict Cairo to the exposed area; avoid extra work
        cr.rectangle(event.area.x, event.area.y,
                event.area.width, event.area.height)
        cr.clip()

        # Refresh sprite list
        self._sprites.redraw_sprites(cr=cr)

    def write_file(self, file_path):
        ''' Clean up '''
        if self._dirty:
            self._save_descriptions_cb()
            self._dirty = False
        if os.path.exists(os.path.join(self.datapath, 'output.ogg')):
            os.remove(os.path.join(self.datapath, 'output.ogg'))

    def do_fullscreen_cb(self, button):
        ''' Hide the Sugar toolbars. '''
        self.fullscreen()

    def invalt(self, x, y, w, h):
        ''' Mark a region for refresh '''
        self._canvas.window.invalidate_rect(
            gtk.gdk.Rectangle(int(x), int(y), int(w), int(h)), False)

    def _spr_to_thumb(self, spr):
        ''' Find which entry in the thumbnails table matches spr. '''
        for i, thumb in enumerate(self._thumbs):
            if spr == thumb[0]:
                return i
        return -1

    def _spr_is_thumbnail(self, spr):
        ''' Does spr match an entry in the thumbnails table? '''
        if self._spr_to_thumb(spr) == -1:
            return False
        else:
            return True

    def _button_press_cb(self, win, event):
        ''' The mouse button was pressed. Is it on a thumbnail sprite? '''
        win.grab_focus()
        x, y = map(int, event.get_coords())

        self._dragpos = [x, y]
        self._total_drag = [0, 0]

        spr = self._sprites.find_sprite((x, y))
        self._press = None
        self._release = None

        # Are we clicking on a thumbnail?
        if not self._spr_is_thumbnail(spr):
            return False

        self.last_spr_moved = spr
        self._press = spr
        self._press.set_layer(DRAG)
        return False

    def _mouse_move_cb(self, win, event):
        """ Drag a thumbnail with the mouse. """
        spr = self._press
        if spr is None:
            self._dragpos = [0, 0]
            return False
        win.grab_focus()
        x, y = map(int, event.get_coords())
        dx = x - self._dragpos[0]
        dy = y - self._dragpos[1]
        spr.move_relative([dx, dy])
        # Also move the star
        self._dragpos = [x, y]
        self._total_drag[0] += dx
        self._total_drag[1] += dy
        return False

    def _button_release_cb(self, win, event):
        ''' Button event is used to swap slides or goto next slide. '''
        win.grab_focus()
        self._dragpos = [0, 0]
        x, y = map(int, event.get_coords())

        if self._thumbnail_mode:
            if self._press is None:
                return
            # Drop the dragged thumbnail below the other thumbnails so
            # that you can find the thumbnail beneath it.
            self._press.set_layer(UNDRAG)
            i = self._spr_to_thumb(self._press)
            spr = self._sprites.find_sprite((x, y))
            if self._spr_is_thumbnail(spr):
                self._release = spr
                # If we found a thumbnail and it is not the one we
                # dragged, swap their positions.
                if not self._press == self._release:
                    j = self._spr_to_thumb(self._release)
                    self._thumbs[i][0] = self._release
                    self._thumbs[j][0] = self._press
                    tmp = self.slides[i]
                    self.slides[i] = self.slides[j]
                    self.slides[j] = tmp
                    self._thumbs[j][0].move((self._thumbs[j][1],
                                             self._thumbs[j][2]))
            self._thumbs[i][0].move((self._thumbs[i][1], self._thumbs[i][2]))
            self._press.set_layer(TOP)
            self._press = None
            self._release = None
        else:
            self._next_cb()
        return False

    def _unit_combo_cb(self, arg=None):
        ''' Read value of predefined conversion factors from combo box '''
        if hasattr(self, '_unit_combo'):
            active = self._unit_combo.get_active()
            if active in UNIT_DICTIONARY:
                self._rate = UNIT_DICTIONARY[active][1]

    def _record_cb(self, button=None):
        ''' Start/stop audio recording '''
        if self._grecord is None:
            _logger.debug('setting up grecord')
            self._grecord = Grecord(self)
        if self._recording:  # Was recording, so stop (and save?)
            _logger.debug('recording...True. Preparing to save.')
            self._grecord.stop_recording_audio()
            self._recording = False
            self._record_button.set_icon('media-record')
            self._record_button.set_tooltip(_('Start recording'))
            _logger.debug('Autosaving recording')
            self._notify(title=_('Save recording'))
            gobject.timeout_add(100, self._wait_for_transcoding_to_finish)
        else:  # Wasn't recording, so start
            _logger.debug('recording...False. Start recording.')
            self._grecord.record_audio()
            self._recording = True
            self._record_button.set_icon('media-recording')
            self._record_button.set_tooltip(_('Stop recording'))

    def _wait_for_transcoding_to_finish(self, button=None):
        while not self._grecord.transcoding_complete():
            time.sleep(1)
        if self._alert is not None:
            self.remove_alert(self._alert)
            self._alert = None
        self._save_recording()

    def _playback_recording_cb(self, button=None, nick=profile.get_nick_name()):
        ''' Play back current recording '''
        _logger.debug('Playback current recording from %s...' % (nick))
        if nick in self._audio_recordings:
            play_audio_from_file(self._audio_recordings[nick])
        return

    def _get_audio_obj_id(self):
        ''' Find unique name for audio object '''
        if 'activity_id' in self.metadata:
            obj_id = self.metadata['activity_id']
        else:
            obj_id = _('Bulletin Board')
        _logger.debug(obj_id)
        return obj_id

    def _save_recording(self):
        if os.path.exists(os.path.join(self.datapath, 'output.ogg')):
            _logger.debug('Saving recording to Journal...')
            obj_id = self._get_audio_obj_id()
            copyfile(os.path.join(self.datapath, 'output.ogg'),
                     os.path.join(self.datapath, '%s.ogg' % (obj_id)))
            dsobject = self._search_for_audio_note(obj_id)
            if dsobject is None:
                dsobject = datastore.create()
            if dsobject is not None:
                _logger.debug(self.dsobjects[self.i].metadata['title'])
                dsobject.metadata['title'] = _('Audio recording by %s') % \
                    (self.metadata['title'])
                dsobject.metadata['icon-color'] = \
                    profile.get_color().to_string()
                dsobject.metadata['tags'] = obj_id
                dsobject.metadata['mime_type'] = 'audio/ogg'
                dsobject.set_file_path(
                    os.path.join(self.datapath, '%s.ogg' % (obj_id)))
                datastore.write(dsobject)
                dsobject.destroy()
            self._add_playback_button(
                profile.get_nick_name(), self.colors,
                os.path.join(self.datapath, '%s.ogg' % (obj_id)))
            if hasattr(self, 'chattube') and self.chattube is not None:
                self._share_audio()
        else:
            _logger.debug('Nothing to save...')
        return

    def _search_for_audio_note(self, obj_id):
        ''' Look to see if there is already a sound recorded for this
        dsobject '''
        dsobjects, nobjects = datastore.find({'mime_type': ['audio/ogg']})
        # Look for tag that matches the target object id
        for dsobject in dsobjects:
            if 'tags' in dsobject.metadata and \
               obj_id in dsobject.metadata['tags']:
                _logger.debug('Found audio note')
                return dsobject
        return None

    def _save_descriptions_cb(self, button=None):
        ''' Find the object in the datastore and write out the changes
        to the decriptions. '''
        for s in self.slides:
            if not s.owner:
                continue
            jobject = datastore.get(s.uid)
            jobject.metadata['description'] = s.desc
            datastore.write(jobject, update_mtime=False,
                            reply_handler=self.datastore_write_cb,
                            error_handler=self.datastore_write_error_cb)

    def datastore_write_cb(self):
        pass

    def datastore_write_error_cb(self, error):
        _logger.error('datastore_write_error_cb: %r' % error)

    def _notify(self, title='', msg=''):
        ''' Notify user when saves are completed '''
        self._alert = Alert()
        self._alert.props.title = title
        self._alert.props.msg = msg
        self.add_alert(self._alert)
        self._alert.show()

    def _resend_cb(self, button=None):
        ''' Resend slides, but only of sharing '''
        if hasattr(self, 'chattube') and self.chattube is not None:
            self._share_slides()
            self._share_audio()

    # Serialize

    def _dump(self, slide):
        ''' Dump data for sharing.'''
        _logger.debug('dumping %s' % (slide.uid))
        data = [slide.uid, slide.colors, slide.title,
                pixbuf_to_base64(activity, slide.pixbuf), slide.desc]
        return self._data_dumper(data)

    def _data_dumper(self, data):
        if _OLD_SUGAR_SYSTEM:
            return json.write(data)
        else:
            io = StringIO()
            jdump(data, io)
            return io.getvalue()

    def _load(self, data):
        ''' Load game data from the journal. '''
        slide = self._data_loader(data)
        if len(slide) == 5:
            if not self._slide_search(slide[0]):
                _logger.debug('loading %s' % (slide[0]))
                self.slides.append(Slide(
                        False, slide[0], slide[1], slide[2],
                        base64_to_pixbuf(activity, slide[3]), slide[4]))

    def _slide_search(self, uid):
        ''' Is this slide in the list already? '''
        for slide in self.slides:
            if slide.uid == uid:
                _logger.debug('skipping %s' % (slide.uid))
                return True
        return False

    def _data_loader(self, data):
        if _OLD_SUGAR_SYSTEM:
            return json.read(data)
        else:
            io = StringIO(data)
            return jload(io)

    # Sharing-related methods

    def _setup_presence_service(self):
        ''' Setup the Presence Service. '''
        self.pservice = presenceservice.get_instance()
        self.initiating = None  # sharing (True) or joining (False)

        owner = self.pservice.get_owner()
        self.owner = owner
        self.buddies = [owner]
        self._share = ''
        self.connect('shared', self._shared_cb)
        self.connect('joined', self._joined_cb)

    def _shared_cb(self, activity):
        ''' Either set up initial share...'''
        if self._shared_activity is None:
            _logger.error('Failed to share or join activity ... \
                _shared_activity is null in _shared_cb()')
            return

        self.initiating = True
        self.waiting = False
        _logger.debug('I am sharing...')

        self.conn = self._shared_activity.telepathy_conn
        self.tubes_chan = self._shared_activity.telepathy_tubes_chan
        self.text_chan = self._shared_activity.telepathy_text_chan

        self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal(
            'NewTube', self._new_tube_cb)

        _logger.debug('This is my activity: making a tube...')
        id = self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube(
            SERVICE, {})

    def _joined_cb(self, activity):
        ''' ...or join an exisiting share. '''
        if self._shared_activity is None:
            _logger.error('Failed to share or join activity ... \
                _shared_activity is null in _shared_cb()')
            return

        self.initiating = False
        _logger.debug('I joined a shared activity.')

        self.conn = self._shared_activity.telepathy_conn
        self.tubes_chan = self._shared_activity.telepathy_tubes_chan
        self.text_chan = self._shared_activity.telepathy_text_chan

        self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal(\
            'NewTube', self._new_tube_cb)

        _logger.debug('I am joining an activity: waiting for a tube...')
        self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
            reply_handler=self._list_tubes_reply_cb,
            error_handler=self._list_tubes_error_cb)

        self.waiting = True

    def _list_tubes_reply_cb(self, tubes):
        ''' Reply to a list request. '''
        for tube_info in tubes:
            self._new_tube_cb(*tube_info)

    def _list_tubes_error_cb(self, e):
        ''' Log errors. '''
        _logger.error('ListTubes() failed: %s', e)

    def _new_tube_cb(self, id, initiator, type, service, params, state):
        ''' Create a new tube. '''
        _logger.debug('New tube: ID=%d initator=%d type=%d service=%s '
                     'params=%r state=%d', id, initiator, type, service,
                     params, state)

        if (type == telepathy.TUBE_TYPE_DBUS and service == SERVICE):
            if state == telepathy.TUBE_STATE_LOCAL_PENDING:
                self.tubes_chan[ \
                              telepathy.CHANNEL_TYPE_TUBES].AcceptDBusTube(id)

            tube_conn = TubeConnection(self.conn,
                self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES], id, \
                group_iface=self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP])

            self.chattube = ChatTube(tube_conn, self.initiating, \
                self.event_received_cb)

            if self.waiting:
                self._send_event('j:%s' % (profile.get_nick_name()))

    def event_received_cb(self, text):
        ''' Data is passed as tuples: cmd:text '''
        _logger.debug('<<< %s' % (text[0]))
        if text[0] == 's':  # shared journal objects
            e, data = text.split(':')
            self._load(data)
        elif text[0] == 'j':  # Someone new has joined
            e, buddy = text.split(':')
            _logger.debug('%s has joined' % (buddy))
            if buddy not in self._buddies:
                self._buddies.append(buddy)
            if self.initiating:
                self._send_event('J:%s' % (profile.get_nick_name()))
                self._share_slides()
                self._share_audio()
        elif text[0] == 'J':  # Everyone must share
            e, buddy = text.split(':')
            self.waiting = False
            if buddy not in self._buddies:
                self._buddies.append(buddy)
                _logger.debug('%s has joined' % (buddy))
            self._share_slides()
            self._share_audio()
        elif text[0] == 'a':  # audio recording
            e, data = text.split(':')
            nick, colors, base64 = self._data_loader(data)
            path = os.path.join(activity.get_activity_root(),
                                'instance', 'nick.ogg')
            base64_to_file(activity, base64, path)
            self._add_playback_button(nick, colors, path)

    def _share_audio(self):
        if profile.get_nick_name() in self._audio_recordings:
            base64 = file_to_base64(
                    activity, self._audio_recordings[profile.get_nick_name()])
            gobject.idle_add(self._send_event, 'a:' + str(
                    self._data_dumper([profile.get_nick_name(),
                                       self.colors,
                                       base64])))

    def _share_slides(self):
        for s in self.slides:
            if s.owner:  # Maybe stagger the timing of the sends?
                gobject.idle_add(self._send_event, 's:' + str(self._dump(s)))
        _logger.debug('finished sharing')

    def _send_event(self, text):
        ''' Send event through the tube. '''
        if hasattr(self, 'chattube') and self.chattube is not None:
            _logger.debug('>>> %s' % (text[0]))
            self.chattube.SendText(text)
Exemple #7
0
class Bounce():
    ''' The Bounce class is used to define the ball and the user
    interaction. '''
    def __init__(self, canvas, path, parent=None):
        ''' Initialize the canvas and set up the callbacks. '''
        self._activity = parent
        self._fraction = None
        self._path = path

        if parent is None:  # Starting from command line
            self._sugar = False
        else:  # Starting from Sugar
            self._sugar = True

        self._canvas = canvas
        self._canvas.grab_focus()

        self._canvas.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        self._canvas.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK)
        self._canvas.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
        self._canvas.add_events(Gdk.EventMask.KEY_PRESS_MASK)
        self._canvas.add_events(Gdk.EventMask.KEY_RELEASE_MASK)
        self._canvas.connect('draw', self.__draw_cb)
        self._canvas.connect('button-press-event', self._button_press_cb)
        self._canvas.connect('button-release-event', self._button_release_cb)
        self._canvas.connect('key-press-event', self._keypress_cb)
        self._canvas.connect('key-release-event', self._keyrelease_cb)
        self._canvas.set_can_focus(True)
        self._canvas.grab_focus()

        self._sprites = Sprites(self._canvas)

        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - GRID_CELL_SIZE
        self._scale = Gdk.Screen.height() / 900.0

        self._step_sid = None  # repeating timeout between steps of ball move
        self._bounce_sid = None  # one-off timeout between bounces
        self.buddies = []  # used for sharing
        self._my_turn = False
        self.select_a_fraction = False

        self._easter_egg = int(uniform(1, 100))

        # Find paths to sound files
        self._path_to_success = os.path.join(path, LAUGH)
        self._path_to_failure = os.path.join(path, CRASH)
        self._path_to_bubbles = os.path.join(path, BUBBLES)

        self._create_sprites(path)

        self.mode = 'fractions'

        self._challenge = 0
        self._expert = False
        self._challenges = []
        for challenge in CHALLENGES[self._challenge]:
            self._challenges.append(challenge)
        self._fraction = 0.5  # the target of the current challenge
        self._label = '1/2'  # the label
        self.count = 0  # number of bounces played
        self._correct = 0  # number of correct answers
        self._press = None  # sprite under mouse click
        self._new_bounce = False
        self._n = 0
        self._accelerometer = self._check_accelerometer()
        self._accel_index = 0
        self._accel_flip = False
        self._accel_xy = [0, 0]
        self._guess_orientation()

        self._dx = 0.  # ball horizontal trajectory
        # acceleration (with dampening)
        self._ddy = (6.67 * self._height) / (STEPS * STEPS)
        self._dy = self._ddy * (1 - STEPS) / 2.  # initial step size

        if self._sugar:
            if _is_tablet_mode():
                self._activity.reset_label(
                    _('Click the ball to start. Rock the computer left '
                      'and right to move the ball.'))
            else:
                self._activity.reset_label(
                    _('Click the ball to start. Then use the arrow keys to '
                      'move the ball.'))

        self._keyrelease_id = None

    def _check_accelerometer(self):
        return os.path.exists(ACCELEROMETER_DEVICE) and _is_tablet_mode()

    def configure_cb(self, event):
        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - GRID_CELL_SIZE
        self._scale = Gdk.Screen.height() / 900.0

        # We need to resize the backgrounds
        width, height = self._calc_background_size()
        for bg in list(self._backgrounds.keys()):
            if bg == 'custom':
                path = self._custom_dsobject.file_path
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                    path, width, height)
            else:
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                    os.path.join(self._path, 'images', bg), width, height)
            if Gdk.Screen.height() > Gdk.Screen.width():
                pixbuf = self._crop_to_portrait(pixbuf)

            self._backgrounds[bg] = pixbuf

        self._background = Sprite(self._sprites, 0, 0,
                                  self._backgrounds[self._current_bg])
        self._background.set_layer(-100)
        self._background.type = 'background'

        # and resize and reposition the bars
        self.bar.resize_all()
        self.bar.show_bar(2)
        self._current_bar = self.bar.get_bar(2)

        # Calculate a new accerlation based on screen height.
        self._ddy = (6.67 * self._height) / (STEPS * STEPS)

        self._guess_orientation()

    def _create_sprites(self, path):
        ''' Create all of the sprites we'll need '''
        self.smiley_graphic = svg_str_to_pixbuf(
            svg_from_file(os.path.join(path, 'images', 'smiley.svg')))

        self.frown_graphic = svg_str_to_pixbuf(
            svg_from_file(os.path.join(path, 'images', 'frown.svg')))

        self.blank_graphic = svg_str_to_pixbuf(
            svg_header(REWARD_HEIGHT, REWARD_HEIGHT, 1.0) + svg_rect(
                REWARD_HEIGHT, REWARD_HEIGHT, 5, 5, 0, 0, 'none', 'none') +
            svg_footer())

        self.ball = Ball(self._sprites,
                         os.path.join(path, 'images', 'soccerball.svg'))
        self._current_frame = 0

        self.bar = Bar(self._sprites, self.ball.width(), COLORS)
        self._current_bar = self.bar.get_bar(2)

        self.ball_y_max = \
            self.bar.bar_y() - self.ball.height() + int(BAR_HEIGHT / 2.)
        self.ball.move_ball((int(
            (self._width - self.ball.width()) // 2), self.ball_y_max))

        self._backgrounds = {}
        width, height = self._calc_background_size()
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
            os.path.join(path, 'images', 'grass_background.png'), width,
            height)
        if Gdk.Screen.height() > Gdk.Screen.width():
            pixbuf = self._crop_to_portrait(pixbuf)

        self._backgrounds['grass_background.png'] = pixbuf

        self._background = Sprite(self._sprites, 0, 0, pixbuf)
        self._background.set_layer(-100)
        self._background.type = 'background'
        self._current_bg = 'grass_background.png'

    def _crop_to_portrait(self, pixbuf):
        tmp = GdkPixbuf.Pixbuf.new(0, True, 8, Gdk.Screen.width(),
                                   Gdk.Screen.height())
        x = int(Gdk.Screen.height() // 3)
        pixbuf.copy_area(x, 0, Gdk.Screen.width(), Gdk.Screen.height(), tmp, 0,
                         0)
        return tmp

    def _calc_background_size(self):
        if Gdk.Screen.height() > Gdk.Screen.width():
            height = Gdk.Screen.height()
            return int(4 * height // 3), height
        else:
            width = Gdk.Screen.width()
            return width, int(3 * width // 4)

    def new_background_from_image(self, path, dsobject=None):
        if path is None:
            path = dsobject.file_path
        width, height = self._calc_background_size()
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(path, width, height)

        if Gdk.Screen.height() > Gdk.Screen.width():
            pixbuf = self._crop_to_portrait(pixbuf)

        self._backgrounds['custom'] = pixbuf
        self.set_background('custom')
        self._custom_dsobject = dsobject
        self._current_bg = 'custom'

    def set_background(self, name):
        if name not in self._backgrounds:
            width, height = self._calc_background_size()
            pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                os.path.join(self._path, 'images', name), width, height)
            if Gdk.Screen.height() > Gdk.Screen.width():
                pixbuf = self._crop_to_portrait(pixbuf)
            self._backgrounds[name] = pixbuf
        self._background.set_image(self._backgrounds[name])
        self.bar.mark.hide()
        self._current_bar.hide()
        self.ball.ball.hide()
        self.ball.ball.set_layer(3)
        self._current_bar.set_layer(2)
        self.bar.mark.set_layer(1)
        self._current_bg = name
        self._canvas.queue_draw()

    def pause(self):
        ''' Pause play when visibility changes '''
        if self._step_sid is not None:
            GLib.source_remove(self._step_sid)
            self._step_sid = None

        if self._bounce_sid is not None:
            GLib.source_remove(self._bounce_sid)
            self._bounce_sid = None

    def we_are_sharing(self):
        ''' If there is more than one buddy, we are sharing. '''
        if len(self.buddies) > 1:
            return True

    def its_my_turn(self):
        ''' When sharing, it is your turn... '''
        GLib.timeout_add(1000, self._take_a_turn)

    def _take_a_turn(self):
        ''' On your turn, choose a fraction. '''
        self._my_turn = True
        self.select_a_fraction = True
        self._activity.set_player_on_toolbar(self._activity.nick,
                                             self._activity.key)
        self._activity.reset_label(_("Click on the bar to choose a fraction."))

    def its_their_turn(self, nick, key):
        ''' When sharing, it is nick's turn... '''
        GLib.timeout_add(1000, self._wait_your_turn, nick, key)

    def _wait_your_turn(self, nick, key):
        ''' Wait for nick to choose a fraction. '''
        self._my_turn = False
        self._activity.set_player_on_toolbar(nick, key)
        self._activity.reset_label(
            _('Waiting for %(buddy)s') % {'buddy': nick})

    def play_a_fraction(self, fraction):
        ''' Play this fraction '''
        fraction_is_new = True
        for i, c in enumerate(self._challenges):
            if c[0] == fraction:
                fraction_is_new = False
                self._n = i
                break
        if fraction_is_new:
            self.add_fraction(fraction)
            self._n = len(self._challenges)
        self._choose_a_fraction()
        self._start_step()

    def _button_press_cb(self, win, event):
        ''' Callback to handle the button presses '''
        win.grab_focus()
        x, y = list(map(int, event.get_coords()))
        self._press = self._sprites.find_sprite((x, y))
        return True

    def _button_release_cb(self, win, event):
        ''' Callback to handle the button releases '''
        win.grab_focus()
        x, y = list(map(int, event.get_coords()))
        if self._press is not None:
            if self.we_are_sharing():
                if self.select_a_fraction and self._press == self._current_bar:
                    # Find the fraction closest to the click
                    fraction = self._search_challenges(
                        (x - self.bar.bar_x()) / float(self.bar.width()))
                    self.select_a_fraction = False
                    self._activity.send_a_fraction(fraction)
                    self.play_a_fraction(fraction)
            else:
                if self._step_sid is None and \
                   self._bounce_sid is None and \
                   self._press == self.ball.ball:
                    self._choose_a_fraction()
                    self._start_step()
        return True

    def _search_challenges(self, f):
        ''' Find the fraction which is closest to f in the list. '''
        dist = 1.
        closest = '1/2'
        for c in self._challenges:
            numden = c[0].split('/')
            delta = abs((float(numden[0]) / float(numden[1])) - f)
            if delta <= dist:
                dist = delta
                closest = c[0]
        return closest

    def _guess_orientation(self):
        if self._accelerometer:
            fh = open(ACCELEROMETER_DEVICE)
            string = fh.read()
            fh.close()
            xyz = string[1:-2].split(',')
            x = int(xyz[0])
            y = int(xyz[1])
            self._accel_xy = [x, y]
            if abs(x) > abs(y):
                self._accel_index = 1  # Portrait mode
                self._accel_flip = x > 0
            else:
                self._accel_index = 0  # Landscape mode
                self._accel_flip = y < 0

    def _defer_bounce(self, ms):
        ''' Pause and then start the ball again '''
        self._bounce_sid = GLib.timeout_add(ms, self._bounce)

    def _bounce(self):
        ''' Start the ball again '''
        self._accelerometer = self._check_accelerometer()
        self._start_step()
        self._bounce_sid = None
        return False

    def _start_step(self):
        ''' Start the ball and keep moving until boundary conditions '''
        if self._step():
            self._step_sid = GLib.timeout_add(STEP_PAUSE, self._step)

    def _step(self):
        ''' Move the ball once and test boundary conditions '''
        if self._new_bounce:
            self.bar.mark.move((0, self._height))  # hide the mark
            if not self.we_are_sharing():
                self._choose_a_fraction()
            self._new_bounce = False
            self._dy = self._ddy * (1 - STEPS) / 2  # initial step size

        if self._accelerometer:
            self._guess_orientation()
            self._dx = float(self._accel_xy[self._accel_index]) / 18.
            if self._accel_flip:
                self._dx *= -1

        if self.ball.ball_x() + self._dx > 0 and \
           self.ball.ball_x() + self._dx < self._width - self.ball.width():
            self.ball.move_ball_relative((int(self._dx), int(self._dy)))
        else:
            self.ball.move_ball_relative((0, int(self._dy)))

        # speed up ball in x while key is pressed
        self._dx *= DDX

        # accelerate in y
        self._dy += self._ddy

        # Calculate a new ball_y_max depending on the x position
        self.ball_y_max = \
            self.bar.bar_y() - self.ball.height() + self._wedge_offset()

        if self.ball.ball_y() >= self.ball_y_max:
            # hit the bottom
            self.ball.move_ball((self.ball.ball_x(), self.ball_y_max))
            self._test()
            self._new_bounce = True

            if self.we_are_sharing():
                if self._my_turn:
                    # Let the next player know it is their turn.
                    i = (self.buddies.index(
                        [self._activity.nick, self._activity.key]) + 1) % \
                        len(self.buddies)
                    [nick, key] = self.buddies[i]
                    self.its_their_turn(nick, key)
                    self._activity.send_event('t', self.buddies[i])
            else:
                if not self.we_are_sharing() and self._easter_egg_test():
                    self._animate()
                else:
                    ms = max(STEP_PAUSE,
                             BOUNCE_PAUSE - self.count * STEP_PAUSE)
                    self._defer_bounce(ms)
            self._step_sid = None
            return False
        else:
            return True

    def _wedge_offset(self):
        return int(BAR_HEIGHT *
                   (1 - (self.ball.ball_x() / float(self.bar.width()))))

    def _mark_offset(self, x):
        return int(BAR_HEIGHT * (1 - (x / float(self.bar.width())))) - 12

    def _animate(self):
        ''' A little Easter Egg just for fun. '''
        if self._new_bounce:
            self._dy = self._ddy * (1 - STEPS) / 2  # initial step size
            self._new_bounce = False
            self._current_frame = 0
            self._frame_counter = 0
            self.ball.move_frame(self._current_frame,
                                 (self.ball.ball_x(), self.ball.ball_y()))
            self.ball.move_ball((self.ball.ball_x(), self._height))
            aplay.play(self._path_to_bubbles)

        if self._accelerometer:
            fh = open(ACCELEROMETER_DEVICE)
            string = fh.read()
            xyz = string[1:-2].split(',')
            self._dx = float(xyz[0]) / 18.
            fh.close()
        else:
            self._dx = uniform(-int(DX * self._scale), int(DX * self._scale))
        self.ball.move_frame_relative(self._current_frame,
                                      (int(self._dx), int(self._dy)))
        self._dy += self._ddy

        self._frame_counter += 1
        self._current_frame = self.ball.next_frame(self._frame_counter)

        if self.ball.frame_y(self._current_frame) >= self.ball_y_max:
            # hit the bottom
            self.ball.move_ball((self.ball.ball_x(), self.ball_y_max))
            self.ball.hide_frames()
            self._test(easter_egg=True)
            self._new_bounce = True
            self._defer_bounce(BOUNCE_PAUSE)
        else:
            GLib.timeout_add(STEP_PAUSE, self._animate)

    def add_fraction(self, string):
        ''' Add a new challenge; set bar to 2x demominator '''
        numden = string.split('/', 2)
        self._challenges.append([string, int(numden[1]), 0])

    def _get_new_fraction(self):
        ''' Select a new fraction challenge from the table '''
        if not self.we_are_sharing():
            n = int(uniform(0, len(self._challenges)))
        else:
            n = self._n
        fstr = self._challenges[n][0]
        if '/' in fstr:  # fraction
            numden = fstr.split('/', 2)
            fraction = float(numden[0].strip()) / float(numden[1].strip())
        elif '%' in fstr:  # percentage
            fraction = float(fstr.strip().strip('%').strip()) / 100.
        else:  # To do: add support for decimals (using locale)
            _logger.debug('Could not parse challenge (%s)', fstr)
            fstr = '1/2'
            fraction = 0.5
        return fraction, fstr, n

    def _choose_a_fraction(self):
        ''' choose a new fraction and set the corresponding bar '''
        # Don't repeat the same fraction twice in a row
        fraction, fstr, n = self._get_new_fraction()
        if not self.we_are_sharing():
            while fraction == self._fraction:
                fraction, fstr, n = self._get_new_fraction()

        self._fraction = fraction
        self._n = n
        if self.mode == 'percents':
            self._label = str(int(self._fraction * 100 + 0.5)) + '%'
        else:  # percentage
            self._label = fstr
        if self.mode == 'sectors':
            self.ball.new_ball_from_fraction(self._fraction)

        if not Gdk.Screen.width() < 1024:
            self._activity.reset_label(
                _('Bounce the ball to a position '
                  '%(fraction)s of the way from the left side of the bar.') %
                {'fraction': self._label})
        else:
            self._activity.reset_label(
                _('Bounce the ball to %(fraction)s') %
                {'fraction': self._label})

        self.ball.ball.set_label(self._label)

        self.bar.hide_bars()
        if self._expert:  # Show two-segment bar in expert mode
            nseg = 2
        else:
            if self.mode == 'percents':
                nseg = 10
            else:
                nseg = self._challenges[self._n][1]
        # generate new bar on demand
        self._current_bar = self.bar.get_bar(nseg)
        self.bar.show_bar(nseg)

    def _easter_egg_test(self):
        ''' Test to see if we show the Easter Egg '''
        delta = self.ball.width() / 8
        x = self.ball.ball_x() + self.ball.width() / 2
        f = self.bar.width() * self._easter_egg / 100.
        if x > f - delta and x < f + delta:
            return True
        else:
            return False

    def _test(self, easter_egg=False):
        ''' Test to see if we estimated correctly '''
        if self._expert:
            delta = self.ball.width() / 6
        else:
            delta = self.ball.width() / 3

        x = self.ball.ball_x() + self.ball.width() / 2
        f = int(self._fraction * self.bar.width())
        self.bar.mark.move((int(f - self.bar.mark_width() / 2),
                            int(self.bar.bar_y() + self._mark_offset(f))))
        if self._challenges[self._n][2] == 0:  # label the column
            spr = Sprite(self._sprites, 0, 0, self.blank_graphic)
            spr.set_label(self._label)
            spr.move((int(self._n * 27), 0))
            spr.set_layer(-1)
        self._challenges[self._n][2] += 1
        if x > f - delta and x < f + delta:
            spr = Sprite(self._sprites, 0, 0, self.smiley_graphic)
            self._correct += 1
            aplay.play(self._path_to_success)
        else:
            spr = Sprite(self._sprites, 0, 0, self.frown_graphic)
            aplay.play(self._path_to_failure)

        spr.move((int(self._n * 27), int(self._challenges[self._n][2] * 27)))
        spr.set_layer(-1)

        # after enough correct answers, up the difficulty
        if self._correct == len(self._challenges) * 2:
            self._challenge += 1
            if self._challenge < len(CHALLENGES):
                for challenge in CHALLENGES[self._challenge]:
                    self._challenges.append(challenge)
            else:
                self._expert = True

        self.count += 1
        self._dx = 0.  # stop horizontal movement between bounces

    def _keypress_cb(self, area, event):
        ''' Keypress: moving the slides with the arrow keys '''
        k = Gdk.keyval_name(event.keyval)
        if k in ['KP_Page_Down', 'KP_Home', 'h', 'Left', 'KP_Left']:
            self._dx = -DX * self._scale
        elif k in ['KP_Page_Up', 'KP_End', 'l', 'Right', 'KP_Right']:
            self._dx = DX * self._scale
        elif k in ['Return']:
            if self._step_sid:
                self._dy = -self._ddy * (1 - STEPS) * 2.
        else:
            self._dx = 0.
        if self._accel_flip:
            self._dx = -self._dx
        return True

    def _keyrelease_cb(self, area, event):
        ''' Keyrelease: stop horizontal movement '''
        def timer_cb():
            self._dx = 0.
            self._keyrelease_id = None
            return False

        if self._keyrelease_id is not None:
            GLib.source_remove(self._keyrelease_id)
        self._keyrelease_id = GLib.timeout_add(100, timer_cb)

        return True

    def __draw_cb(self, canvas, cr):
        self._sprites.redraw_sprites(cr=cr)

    def _destroy_cb(self, win, event):
        ''' Callback to handle quit '''
        Gtk.main_quit()
class Game():
    ''' OLPC XO man color changer designed in memory of Nat Jacobson '''
    def __init__(self, canvas, parent=None, mycolors=['#A0FFA0', '#FF8080']):
        self._activity = parent
        self.colors = [mycolors[0]]
        self.colors.append(mycolors[1])

        self._canvas = canvas
        if parent is not None:
            parent.show_all()
            self._parent = parent

        self._canvas.connect("draw", self.__draw_cb)
        self._canvas.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        self._canvas.connect("button-press-event", self._button_press_cb)
        self._canvas.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK)
        self._canvas.connect('button-release-event', self._button_release_cb)
        self._canvas.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
        self._canvas.connect("motion-notify-event", self._mouse_move_cb)

        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - GRID_CELL_SIZE
        self._scale = self._width / 1200.

        self.press = None
        self.dragpos = [0, 0]
        self.startpos = [0, 0]

        self._dot_cache = {}
        self._xo_cache = {}

        self._radius = 22.5
        self._stroke_width = 9.5

        # Generate the sprites we'll need...
        self._sprites = Sprites(self._canvas)
        self._dots = []
        self._xo_man = None
        self._generate_bg('#FFF')

        # First dot, starting angle
        self._cxy = [self._width / 2, self._height / 2]
        self._xy = [
            self._width / 2 + 120 * self._scale,
            self._height / 2 - self._radius * self._scale
        ]
        self._angle = 0
        self._dot_size_plus = self._radius * 3 * self._scale
        self._min = -self._dot_size_plus / 3
        self._max = self._height - (self._dot_size_plus / 2.2)

        self._zones = []
        self._calc_zones()
        self._generate_spiral()

    def _calc_zones(self):
        for color in colors:
            rgb1 = _from_hex(color[0])
            rgb2 = _from_hex(color[1])
            dv = _contrast(rgb1, rgb2)
            dh = _delta_hue(rgb1, rgb2)
            self._zones.append(_zone(dv, dh))

    def _calc_next_dot_position(self):
        ''' calculate spiral coordinates '''
        dx = self._xy[0] - self._cxy[0]
        dy = self._xy[1] - self._cxy[1]
        r = sqrt(dx * dx + dy * dy)
        c = 2 * r * pi
        a = atan2(dy, dx)
        da = (self._dot_size_plus / c) * 2 * pi
        a += da
        r += self._dot_size_plus / (c / self._dot_size_plus)
        self._xy[0] = r * cos(a) + self._cxy[0]
        self._xy[1] = r * sin(a) + self._cxy[1]
        if self._xy[1] < self._min or self._xy[1] > self._max:
            self._calc_next_dot_position()

    def _generate_spiral(self):
        ''' Make a new set of dots for a sprial '''
        for z in range(4):
            for i in range(len(colors)):
                if self._zones[i] == z:
                    self._dots.append(
                        Sprite(self._sprites, self._xy[0], self._xy[1],
                               self._new_dot(colors[i])))
                    self._dots[-1].type = i
                    self._calc_next_dot_position()
        if self._xo_man is None:
            x = 510 * self._scale
            y = 280 * self._scale
            self._xo_man = Sprite(self._sprites, x, y,
                                  self._new_xo_man(self.colors))
            self._xo_man.type = None

    def move_dot(self, i, x, y):
        self._dots[i].move((x, y))

    def get_dot_xy(self, i):
        return self._dots[i].get_xy()

    def move_xo_man(self, x, y):
        self._xo_man.move((x, y))

    def get_xo_man_xy(self):
        return self._xo_man.get_xy()

    def rotate(self):
        x, y = self._dots[0].get_xy()
        for i in range(len(colors) - 1):
            self._dots[i].move(self._dots[i + 1].get_xy())
        self._dots[-1].move((x, y))

    def _generate_bg(self, color):
        ''' a background color '''
        self._bg = Sprite(self._sprites, 0, 0, self._new_background(color))
        self._bg.set_layer(0)
        self._bg.type = None

    def adj_background(self, color):
        ''' Change background '''
        self._bg.set_image(self._new_background(color))
        self._bg.set_layer(0)

    def _button_press_cb(self, win, event):
        win.grab_focus()
        x, y = map(int, event.get_coords())
        self.dragpos = [x, y]

        spr = self._sprites.find_sprite((x, y))
        if spr == None or spr == self._bg:
            return
        self.startpos = spr.get_xy()
        self.press = spr

    def _mouse_move_cb(self, win, event):
        """ Drag a rule with the mouse. """
        if self.press is None:
            self.dragpos = [0, 0]
            return True
        win.grab_focus()
        x, y = map(int, event.get_coords())
        dx = x - self.dragpos[0]
        dy = y - self.dragpos[1]
        self.press.move_relative((dx, dy))
        self.dragpos = [x, y]

    def _button_release_cb(self, win, event):
        if self.press == None:
            return True
        if _distance(self.press.get_xy(), self.startpos) < 20:
            if type(self.press.type) == int:
                self.i = self.press.type
                self._new_surface()
            self.press.move(self.startpos)
        self.press = None

    def _new_surface(self):
        self.colors[0] = colors[self.i][0]
        self.colors[1] = colors[self.i][1]
        self._xo_man.set_image(self._new_xo_man(colors[self.i]))
        self._xo_man.set_layer(100)

    def __draw_cb(self, canvas, cr):
        self._sprites.redraw_sprites(cr=cr)

    def do_expose_event(self, event):
        ''' Handle the expose-event by drawing '''
        # Restrict Cairo to the exposed area
        cr = self._canvas.window.cairo_create()
        cr.rectangle(event.area.x, event.area.y, event.area.width,
                     event.area.height)
        cr.clip()
        # Refresh sprite list
        self._sprites.redraw_sprites(cr=cr)

    def _destroy_cb(self, win, event):
        Gtk.main_quit()

    def _new_dot(self, color):
        ''' generate a dot of a color color '''
        if True:  # not color in self._dot_cache:
            self._stroke = color[0]
            self._fill = color[1]
            self._svg_width = int(60 * self._scale)
            self._svg_height = int(60 * self._scale)
            pixbuf = svg_str_to_pixbuf(
                self._header() + \
                '<circle cx="%f" cy="%f" r="%f" stroke="%s" fill="%s" \
stroke-width="%f" visibility="visible" />'                                           % (
                        30 * self._scale, 30 * self._scale,
                        self._radius * self._scale, self._stroke,
                        self._fill, self._stroke_width * self._scale) + \
                self._footer())

            surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self._svg_width,
                                         self._svg_height)
            context = cairo.Context(surface)
            Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0)
            context.rectangle(0, 0, self._svg_width, self._svg_height)
            context.fill()
            # self._dot_cache[color] = surface

        return surface  # self._dot_cache[color]

    def _new_background(self, color):
        ''' Background color '''
        self._svg_width = int(self._width)
        self._svg_height = int(self._height)
        string = \
            self._header() + \
            '<rect width="%f" height="%f" x="%f" \
y="%f" fill="%s" stroke="none" visibility="visible" />'                                                        % (
                    self._width, self._height, 0, 0, color) + \
            self._footer()
        pixbuf = svg_str_to_pixbuf(string)
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self._svg_width,
                                     self._svg_height)
        context = cairo.Context(surface)
        Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0)
        context.rectangle(0, 0, self._svg_width, self._svg_height)
        context.fill()
        return surface

    def _new_xo_man(self, color):
        ''' generate a xo-man of a color color '''
        if True:  # not color in self._xo_cache:
            self._stroke = color[0]
            self._fill = color[1]
            self._svg_width = int(240. * self._scale)
            self._svg_height = int(260. * self._scale)
            string = \
                self._header() + \
                '<g>' + \
                '<g id="XO">' + \
                '<path id="Line1" d="M%f,%f C%f,%f %f,%f %f,%f" stroke="%s" \
stroke-width="%f" stroke-linecap="round" fill="none" visibility="visible" />' \
% (
                        165.5 * self._scale, 97 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        74.5 * self._scale, 188 * self._scale,
                        self._stroke, 37 * self._scale) + \
                '<path id="Line2" d="M%f,%f C%f,%f %f,%f %f,%f" stroke="%s" \
stroke-width="%f" stroke-linecap="round" fill="none" visibility="visible" />' \
% (
                        165.5 * self._scale, 188 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        74.5 * self._scale, 97 * self._scale,
                        self._stroke, 37 * self._scale) + \
                '<path id="Fill1" d="M%f,%f C%f,%f %f,%f %f,%f" stroke="%s" \
stroke-width="%f" stroke-linecap="round" fill="none" visibility="visible" />' \
% (
                        165.5 * self._scale, 97 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        74.5 * self._scale, 188 * self._scale,
                        self._fill, 17 * self._scale) + \
                '<path id="Fill2" d="M%f,%f C%f,%f %f,%f %f,%f" stroke="%s" \
stroke-width="%f" stroke-linecap="round" fill="none" visibility="visible" />' \
% (
                        165.5 * self._scale, 188 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        74.5 * self._scale, 97 * self._scale,
                        self._fill, 17 * self._scale) + \
                '<circle id="Circle" cx="%f" cy="%f" r="%f" \
fill="%s" stroke="%s" stroke-width="%f" visibility="visible" />'                                                                 % (
                        120 * self._scale, 61.5 * self._scale,
                        27.5 * self._scale,
                        self._fill, self._stroke, 11 * self._scale) + \
                '</g></g>' + \
                self._footer()
            pixbuf = svg_str_to_pixbuf(string)

            surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self._svg_width,
                                         self._svg_height)
            context = cairo.Context(surface)
            Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0)
            context.rectangle(0, 0, self._svg_width, self._svg_height)
            context.fill()
            # self._xo_cache[color] = surface
        return surface  # self._xo_cache[color]

    def _header(self):
        return '<svg\n' + 'xmlns:svg="http:#www.w3.org/2000/svg"\n' + \
            'xmlns="http://www.w3.org/2000/svg"\n' + \
            'xmlns:xlink="http://www.w3.org/1999/xlink"\n' + \
            'version="1.1"\n' + 'width="' + str(self._svg_width) + '"\n' + \
            'height="' + str(self._svg_height) + '">\n'

    def _footer(self):
        return '</svg>\n'
Exemple #9
0
class Bounce():
    ''' The Bounce class is used to define the ball and the user
    interaction. '''

    def __init__(self, canvas, path, parent=None):
        ''' Initialize the canvas and set up the callbacks. '''
        self._activity = parent
        self._fraction = None
        self._path = path

        if parent is None:        # Starting from command line
            self._sugar = False
        else:                     # Starting from Sugar
            self._sugar = True

        self._canvas = canvas
        self._canvas.grab_focus()

        self._canvas.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        self._canvas.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK)
        self._canvas.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
        self._canvas.add_events(Gdk.EventMask.KEY_PRESS_MASK)
        self._canvas.add_events(Gdk.EventMask.KEY_RELEASE_MASK)
        self._canvas.connect('draw', self.__draw_cb)
        self._canvas.connect('button-press-event', self._button_press_cb)
        self._canvas.connect('button-release-event', self._button_release_cb)
        self._canvas.connect('key-press-event', self._keypress_cb)
        self._canvas.connect('key-release-event', self._keyrelease_cb)
        self._canvas.set_can_focus(True)
        self._canvas.grab_focus()

        self._sprites = Sprites(self._canvas)

        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - GRID_CELL_SIZE
        self._scale = Gdk.Screen.height() / 900.0

        self._timeout = None
        self.buddies = []  # used for sharing
        self._my_turn = False
        self.select_a_fraction = False

        self._easter_egg = int(uniform(1, 100))

        # Find paths to sound files
        self._path_to_success = os.path.join(path, LAUGH)
        self._path_to_failure = os.path.join(path, CRASH)
        self._path_to_bubbles = os.path.join(path, BUBBLES)

        self._create_sprites(path)

        self.mode = 'fractions'

        self._challenge = 0
        self._expert = False
        self._challenges = []
        for challenge in CHALLENGES[self._challenge]:
            self._challenges.append(challenge)
        self._fraction = 0.5  # the target of the current challenge
        self._label = '1/2'  # the label
        self.count = 0  # number of bounces played
        self._correct = 0  # number of correct answers
        self._press = None  # sprite under mouse click
        self._new_bounce = False
        self._n = 0
        self._accel_index = 0
        self._accel_flip = False
        self._accel_xy = [0, 0]
        self._guess_orientation()

        self._dx = 0.  # ball horizontal trajectory
        # acceleration (with dampening)
        self._ddy = (6.67 * self._height) / (STEPS * STEPS)
        self._dy = self._ddy * (1 - STEPS) / 2.  # initial step size

        if self._sugar:
            if _is_tablet_mode():
                self._activity.reset_label(
                    _('Click the ball to start. Rock the computer left '
                      'and right to move the ball.'))
            else:
                self._activity.reset_label(
                    _('Click the ball to start. Then use the arrow keys to '
                      'move the ball.'))

    def _accelerometer(self):
        return os.path.exists(ACCELEROMETER_DEVICE) and _is_tablet_mode()

    def configure_cb(self, event):
        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - GRID_CELL_SIZE
        self._scale = Gdk.Screen.height() / 900.0

        # We need to resize the backgrounds
        width, height = self._calc_background_size()
        for bg in self._backgrounds.keys():
            if bg == 'custom':
                path = self._custom_dsobject.file_path
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                    path, width, height)
            else:
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                    os.path.join(self._path, 'images', bg),
                    width, height)
            if Gdk.Screen.height() > Gdk.Screen.width():
                pixbuf = self._crop_to_portrait(pixbuf)

            self._backgrounds[bg] = pixbuf

        self._background = Sprite(self._sprites, 0, 0,
                                  self._backgrounds[self._current_bg])
        self._background.set_layer(-100)
        self._background.type = 'background'

        # and resize and reposition the bars
        self.bar.resize_all()
        self.bar.show_bar(2)
        self._current_bar = self.bar.get_bar(2)

        # Calculate a new accerlation based on screen height.
        self._ddy = (6.67 * self._height) / (STEPS * STEPS)

        self._guess_orientation()

    def _create_sprites(self, path):
        ''' Create all of the sprites we'll need '''
        self.smiley_graphic = svg_str_to_pixbuf(svg_from_file(
            os.path.join(path, 'images', 'smiley.svg')))

        self.frown_graphic = svg_str_to_pixbuf(svg_from_file(
            os.path.join(path, 'images', 'frown.svg')))

        self.blank_graphic = svg_str_to_pixbuf(
            svg_header(REWARD_HEIGHT, REWARD_HEIGHT, 1.0) +
            svg_rect(REWARD_HEIGHT, REWARD_HEIGHT, 5, 5, 0, 0,
                     'none', 'none') +
            svg_footer())

        self.ball = Ball(self._sprites,
                         os.path.join(path, 'images', 'soccerball.svg'))
        self._current_frame = 0

        self.bar = Bar(self._sprites, self.ball.width(), COLORS)
        self._current_bar = self.bar.get_bar(2)

        self.ball_y_max = self.bar.bar_y() - self.ball.height() + \
                          int(BAR_HEIGHT / 2.)
        self.ball.move_ball((int((self._width - self.ball.width()) / 2),
                             self.ball_y_max))

        self._backgrounds = {}
        width, height = self._calc_background_size()
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
            os.path.join(path, 'images', 'grass_background.png'),
            width, height)
        if Gdk.Screen.height() > Gdk.Screen.width():
            pixbuf = self._crop_to_portrait(pixbuf)

        self._backgrounds['grass_background.png'] = pixbuf

        self._background = Sprite(self._sprites, 0, 0, pixbuf)
        self._background.set_layer(-100)
        self._background.type = 'background'
        self._current_bg = 'grass_background.png'

    def _crop_to_portrait(self, pixbuf):
        tmp = GdkPixbuf.Pixbuf.new(0, True, 8, Gdk.Screen.width(),
                                   Gdk.Screen.height())
        x = int(Gdk.Screen.height() / 3)
        pixbuf.copy_area(x, 0, Gdk.Screen.width(), Gdk.Screen.height(),
                         tmp, 0, 0)
        return tmp

    def _calc_background_size(self):
        if Gdk.Screen.height() > Gdk.Screen.width():
            height = Gdk.Screen.height()
            return int(4 * height / 3), height
        else:
            width = Gdk.Screen.width()
            return width, int(3 * width / 4)

    def new_background_from_image(self, path, dsobject=None):
        if path is None:
            path = dsobject.file_path
        width, height = self._calc_background_size()
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
            path, width, height)

        if Gdk.Screen.height() > Gdk.Screen.width():
            pixbuf = self._crop_to_portrait(pixbuf)

        self._backgrounds['custom'] = pixbuf
        self.set_background('custom')
        self._custom_dsobject = dsobject
        self._current_bg = 'custom'

    def set_background(self, name):
        if not name in self._backgrounds:
            width, height = self._calc_background_size()
            pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                os.path.join(self._path, 'images', name), width, height)
            if Gdk.Screen.height() > Gdk.Screen.width():
                pixbuf = self._crop_to_portrait(pixbuf)
            self._backgrounds[name] = pixbuf
        self._background.set_image(self._backgrounds[name])
        self.bar.mark.hide()
        self._current_bar.hide()
        self.ball.ball.hide()
        self.do_expose_event()
        self.ball.ball.set_layer(3)
        self._current_bar.set_layer(2)
        self.bar.mark.set_layer(1)
        self._current_bg = name

    def pause(self):
        ''' Pause play when visibility changes '''
        if self._timeout is not None:
            GObject.source_remove(self._timeout)
            self._timeout = None

    def we_are_sharing(self):
        ''' If there is more than one buddy, we are sharing. '''
        if len(self.buddies) > 1:
            return True

    def its_my_turn(self):
        ''' When sharing, it is your turn... '''
        GObject.timeout_add(1000, self._take_a_turn)

    def _take_a_turn(self):
        ''' On your turn, choose a fraction. '''
        self._my_turn = True
        self.select_a_fraction = True
        self._activity.set_player_on_toolbar(self._activity.nick)
        self._activity.reset_label(
            _("Click on the bar to choose a fraction."))

    def its_their_turn(self, nick):
        ''' When sharing, it is nick's turn... '''
        GObject.timeout_add(1000, self._wait_your_turn, nick)

    def _wait_your_turn(self, nick):
        ''' Wait for nick to choose a fraction. '''
        self._my_turn = False
        self._activity.set_player_on_toolbar(nick)
        self._activity.reset_label(
            _('Waiting for %(buddy)s') % {'buddy': nick})

    def play_a_fraction(self, fraction):
        ''' Play this fraction '''
        fraction_is_new = True
        for i, c in enumerate(self._challenges):
            if c[0] == fraction:
                fraction_is_new = False
                self._n = i
                break
        if fraction_is_new:
            self.add_fraction(fraction)
            self._n = len(self._challenges)
        self._choose_a_fraction()
        self._move_ball()

    def _button_press_cb(self, win, event):
        ''' Callback to handle the button presses '''
        win.grab_focus()
        x, y = map(int, event.get_coords())
        self._press = self._sprites.find_sprite((x, y))
        return True

    def _button_release_cb(self, win, event):
        ''' Callback to handle the button releases '''
        win.grab_focus()
        x, y = map(int, event.get_coords())
        if self._press is not None:
            if self.we_are_sharing():
                if self.select_a_fraction and self._press == self._current_bar:
                    # Find the fraction closest to the click
                    fraction = self._search_challenges(
                        (x - self.bar.bar_x()) / float(self.bar.width()))
                    self.select_a_fraction = False
                    self._activity.send_a_fraction(fraction)
                    self.play_a_fraction(fraction)
            else:
                if self._timeout is None and self._press == self.ball.ball:
                    self._choose_a_fraction()
                    self._move_ball()
        return True

    def _search_challenges(self, f):
        ''' Find the fraction which is closest to f in the list. '''
        dist = 1.
        closest = '1/2'
        for c in self._challenges:
            numden = c[0].split('/')
            delta = abs((float(numden[0]) / float(numden[1])) - f)
            if delta <= dist:
                dist = delta
                closest = c[0]
        return closest

    def _guess_orientation(self):
        if self._accelerometer():
            fh = open(ACCELEROMETER_DEVICE)
            string = fh.read()
            fh.close()
            xyz = string[1:-2].split(',')
            x = int(xyz[0])
            y = int(xyz[1])
            self._accel_xy = [x, y]
            if abs(x) > abs(y):
                self._accel_index = 1  # Portrait mode
                self._accel_flip = x > 0
            else:
                self._accel_index = 0  # Landscape mode
                self._accel_flip = y < 0

    def _move_ball(self):
        ''' Move the ball and test boundary conditions '''
        if self._new_bounce:
            self.bar.mark.move((0, self._height))  # hide the mark
            if not self.we_are_sharing():
                self._choose_a_fraction()
            self._new_bounce = False
            self._dy = self._ddy * (1 - STEPS) / 2  # initial step size

        if self._accelerometer():
            self._guess_orientation()
            self._dx = float(self._accel_xy[self._accel_index]) / 18.
            if self._accel_flip:
                self._dx *= -1

        if self.ball.ball_x() + self._dx > 0 and \
           self.ball.ball_x() + self._dx < self._width - self.ball.width():
            self.ball.move_ball_relative((int(self._dx), int(self._dy)))
        else:
            self.ball.move_ball_relative((0, int(self._dy)))

        # speed up ball in x while key is pressed
        self._dx *= DDX

        # accelerate in y
        self._dy += self._ddy

        # Calculate a new ball_y_max depending on the x position
        self.ball_y_max = self.bar.bar_y() - self.ball.height() + \
                          self._wedge_offset()

        if self.ball.ball_y() >= self.ball_y_max:
            # hit the bottom
            self.ball.move_ball((self.ball.ball_x(), self.ball_y_max))
            self._test()
            self._new_bounce = True

            if self.we_are_sharing():
                if self._my_turn:
                    # Let the next player know it is their turn.
                    i = (self.buddies.index(self._activity.nick) + 1) % \
                        len(self.buddies)
                    self.its_their_turn(self.buddies[i])
                    self._activity.send_event('', {"data": (self.buddies[i])})
            else:
                if not self.we_are_sharing() and self._easter_egg_test():
                    self._animate()
                else:
                    self._timeout = GObject.timeout_add(
                        max(STEP_PAUSE,
                            BOUNCE_PAUSE - self.count * STEP_PAUSE),
                        self._move_ball)
        else:
            self._timeout = GObject.timeout_add(STEP_PAUSE, self._move_ball)

    def _wedge_offset(self):
        return int(BAR_HEIGHT * (1 - (self.ball.ball_x() /
                                      float(self.bar.width()))))

    def _mark_offset(self, x):
        return int(BAR_HEIGHT * (1 - (x / float(self.bar.width())))) - 12

    def _animate(self):
        ''' A little Easter Egg just for fun. '''
        if self._new_bounce:
            self._dy = self._ddy * (1 - STEPS) / 2  # initial step size
            self._new_bounce = False
            self._current_frame = 0
            self._frame_counter = 0
            self.ball.move_frame(self._current_frame,
                                (self.ball.ball_x(), self.ball.ball_y()))
            self.ball.move_ball((self.ball.ball_x(), self._height))
            GObject.idle_add(play_audio_from_file, self, self._path_to_bubbles)

        if self._accelerometer():
            fh = open(ACCELEROMETER_DEVICE)
            string = fh.read()
            xyz = string[1:-2].split(',')
            self._dx = float(xyz[0]) / 18.
            fh.close()
        else:
            self._dx = uniform(-int(DX * self._scale), int(DX * self._scale))
        self.ball.move_frame_relative(
            self._current_frame, (int(self._dx), int(self._dy)))
        self._dy += self._ddy

        self._frame_counter += 1
        self._current_frame = self.ball.next_frame(self._frame_counter)

        if self.ball.frame_y(self._current_frame) >= self.ball_y_max:
            # hit the bottom
            self.ball.move_ball((self.ball.ball_x(), self.ball_y_max))
            self.ball.hide_frames()
            self._test(easter_egg=True)
            self._new_bounce = True
            self._timeout = GObject.timeout_add(BOUNCE_PAUSE, self._move_ball)
        else:
            GObject.timeout_add(STEP_PAUSE, self._animate)

    def add_fraction(self, string):
        ''' Add a new challenge; set bar to 2x demominator '''
        numden = string.split('/', 2)
        self._challenges.append([string, int(numden[1]), 0])

    def _get_new_fraction(self):
        ''' Select a new fraction challenge from the table '''
        if not self.we_are_sharing():
            n = int(uniform(0, len(self._challenges)))
        else:
            n = self._n
        fstr = self._challenges[n][0]
        if '/' in fstr:  # fraction
            numden = fstr.split('/', 2)
            fraction = float(numden[0].strip()) / float(numden[1].strip())
        elif '%' in fstr:  # percentage
            fraction = float(fstr.strip().strip('%').strip()) / 100.
        else:  # To do: add support for decimals (using locale)
            _logger.debug('Could not parse challenge (%s)', fstr)
            fstr = '1/2'
            fraction = 0.5
        return fraction, fstr, n

    def _choose_a_fraction(self):
        ''' choose a new fraction and set the corresponding bar '''
        # Don't repeat the same fraction twice in a row
        fraction, fstr, n = self._get_new_fraction()
        if not self.we_are_sharing():
            while fraction == self._fraction:
                fraction, fstr, n = self._get_new_fraction()

        self._fraction = fraction
        self._n = n
        if self.mode == 'percents':
            self._label = str(int(self._fraction * 100 + 0.5)) + '%'
        else:  # percentage
            self._label = fstr
        if self.mode == 'sectors':
            self.ball.new_ball_from_fraction(self._fraction)

        if not Gdk.Screen.width() < 1024:
            self._activity.reset_label(
                _('Bounce the ball to a position '
                  '%(fraction)s of the way from the left side of the bar.')
                % {'fraction': self._label})
        else:
            self._activity.reset_label(_('Bounce the ball to %(fraction)s')
                                       % {'fraction': self._label})

        self.ball.ball.set_label(self._label)

        self.bar.hide_bars()
        if self._expert:  # Show two-segment bar in expert mode
            nseg = 2
        else:
            if self.mode == 'percents':
                nseg = 10
            else:
                nseg = self._challenges[self._n][1]
        # generate new bar on demand
        self._current_bar = self.bar.get_bar(nseg)
        self.bar.show_bar(nseg)

    def _easter_egg_test(self):
        ''' Test to see if we show the Easter Egg '''
        delta = self.ball.width() / 8
        x = self.ball.ball_x() + self.ball.width() / 2
        f = self.bar.width() * self._easter_egg / 100.
        if x > f - delta and x < f + delta:
            return True
        else:
            return False

    def _test(self, easter_egg=False):
        ''' Test to see if we estimated correctly '''
        self._timeout = None

        if self._expert:
            delta = self.ball.width() / 6
        else:
            delta = self.ball.width() / 3

        x = self.ball.ball_x() + self.ball.width() / 2
        f = int(self._fraction * self.bar.width())
        self.bar.mark.move((int(f - self.bar.mark_width() / 2),
                            int(self.bar.bar_y() + self._mark_offset(f))))
        if self._challenges[self._n][2] == 0:  # label the column
            spr = Sprite(self._sprites, 0, 0, self.blank_graphic)
            spr.set_label(self._label)
            spr.move((int(self._n * 27), 0))
            spr.set_layer(-1)
        self._challenges[self._n][2] += 1
        if x > f - delta and x < f + delta:
            spr = Sprite(self._sprites, 0, 0, self.smiley_graphic)
            self._correct += 1
            GObject.idle_add(play_audio_from_file, self, self._path_to_success)
        else:
            spr = Sprite(self._sprites, 0, 0, self.frown_graphic)
            GObject.idle_add(play_audio_from_file, self, self._path_to_failure)

        spr.move((int(self._n * 27), int(self._challenges[self._n][2] * 27)))
        spr.set_layer(-1)

        # after enough correct answers, up the difficulty
        if self._correct == len(self._challenges) * 2:
            self._challenge += 1
            if self._challenge < len(CHALLENGES):
                for challenge in CHALLENGES[self._challenge]:
                    self._challenges.append(challenge)
            else:
                self._expert = True

        self.count += 1
        self._dx = 0.  # stop horizontal movement between bounces

    def _keypress_cb(self, area, event):
        ''' Keypress: moving the slides with the arrow keys '''
        k = Gdk.keyval_name(event.keyval)
        if k in ['KP_Page_Down', 'KP_Home', 'h', 'Left', 'KP_Left']:
            self._dx = -DX * self._scale
        elif k in ['KP_Page_Up', 'KP_End', 'l', 'Right', 'KP_Right']:
            self._dx = DX * self._scale
        elif k in ['Return']:
            self._choose_a_fraction()
            self._move_ball()
        else:
            self._dx = 0.
        if self._accel_flip:
            self._dx = -self._dx
        return True

    def _keyrelease_cb(self, area, event):
        ''' Keyrelease: stop horizontal movement '''
        self._dx = 0.
        return True

    def __draw_cb(self, canvas, cr):
        self._sprites.redraw_sprites(cr=cr)

    def do_expose_event(self, event=None):
        ''' Handle the expose-event by drawing '''
        # Restrict Cairo to the exposed area
        cr = self._activity.get_window().cairo_create()
        if event is None:
            cr.rectangle(0, 0, Gdk.Screen.width(), Gdk.Screen.height())
        else:
            cr.rectangle(event.area.x, event.area.y,
                         event.area.width, event.area.height)
        cr.clip()
        self._sprites.redraw_sprites(cr=cr)

    def _destroy_cb(self, win, event):
        ''' Callback to handle quit '''
        Gtk.main_quit()