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()
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()
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 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'
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)
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'
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()