# -*- coding: utf-8 -*- #Copyright (c) 2010-12, Walter Bender #Copyright (c) 2010, Tuukka Hastrup # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the # Free Software Foundation, Inc., 59 Temple Place - Suite 330, # Boston, MA 02111-1307, USA. import gi from gi.repository import Gtk, Gdk, GdkPixbuf from math import floor, ceil import locale import traceback import logging _logger = logging.getLogger('abacus-activity') try: from sugar3.graphics import style GRID_CELL_SIZE = style.GRID_CELL_SIZE except ImportError: GRID_CELL_SIZE = 0 from sprites import Sprites, Sprite MAX_RODS = 18 MAX_TOP = 4 MAX_BOT = 14 MAX_BEADS = MAX_RODS * (MAX_TOP + MAX_BOT) BEAD_WIDTH = 40 BEAD_HEIGHT = 30 BEAD_OFFSET = 10 FRAME_STROKE_WIDTH = 45 FRAME_LAYER = 100 ROD_LAYER = 101 BEAD_LAYER = 102 BAR_LAYER = 103 DOT_LAYER = 104 MARK_LAYER = 105 MAX_FADE_LEVEL = 3 CURSOR = '█' ROD_COLORS = ['#282828', '#A0A0A0', '#282828', '#A0A0A0', '#282828', '#A0A0A0', '#282828', '#A0A0A0', '#282828', '#A0A0A0', '#282828', '#A0A0A0', '#282828', '#A0A0A0', '#282828'] COLORS = ('#FFFFFF', '#FF0000', '#88FF00', '#FF00FF', '#FFFF00', '#00CC00', '#000000', '#AA6600', '#00CCFF', '#FF8800') LABELS = ('#000000', '#FFFFFF', '#000000', '#FFFFFF', '#000000', '#000000', '#FFFFFF', '#FFFFFF', '#000000', '#000000') def dec2frac(d): ''' Convert float to its approximate fractional representation. ''' ''' This code was translated to Python from the answers at http://stackoverflow.com/questions/95727/how-to-convert-floats-to-human-\ readable-fractions/681534#681534 For example: >>> 3./5 0.59999999999999998 >>> dec2frac(3./5) "3/5" ''' if d > 1: return '%s' % d df = 1.0 top = 1 bot = 1 while abs(df - d) > 0.00000001: if df < d: top += 1 else: bot += 1 top = int(d * bot) df = float(top) / bot if bot == 1: return '%s' % top elif top == 0: return '' return '%s/%s' % (top, bot) # # Utilities for generating artwork as SVG # def _svg_str_to_pixbuf(svg_string): ''' Load pixbuf from SVG string ''' pl = GdkPixbuf.PixbufLoader.new_with_type('svg') pl.write(svg_string) pl.close() pixbuf = pl.get_pixbuf() return pixbuf def _svg_circle(r, cx, cy, fill, stroke): ''' Returns an SVG circle ''' svg_string = ' \n') svg_string += '%s%f%s%f%s' % ('\n') return svg_string def _svg_footer(): ''' Returns SVG footer ''' svg_string = '\n' svg_string += '\n' return svg_string def _svg_style(extras=''): ''' Returns SVG style for shape rendering ''' return '%s%s%s' % ('style="', extras, '"/>\n') def _calc_fade(bead_color, fade_color, i, n): ''' Fade from bead color to fade color ''' r = i * float.fromhex('0x' + fade_color[1:3]) / n + \ (n - i) * float.fromhex('0x' + bead_color[1:3]) / n g = i * float.fromhex('0x' + fade_color[3:5]) / n + \ (n - i) * float.fromhex('0x' + bead_color[3:5]) / n b = i * float.fromhex('0x' + fade_color[5:]) / n + \ (n - i) * float.fromhex('0x' + bead_color[5:]) / n return '#%02x%02x%02x' % (int(r), int(g), int(b)) class Bead(): ''' The Bead class is used to define the individual beads. ''' def __init__(self): self.spr = None self.state = 0 self.fade_level = 0 self.value = 0 self.offset = 0 def update(self, sprites, pixbuf, offset, value, max_fade=MAX_FADE_LEVEL, tristate=False): ''' We store a sprite, an offset, and a value for each bead. ''' if self.spr is None: self.spr = Sprite(sprites, 0, 0, pixbuf) else: self.spr.set_image(pixbuf) self.spr.set_label('') self.spr.set_label_color('black') self.offset = offset # Decimals will be converted to fractions; # and we want to avoid decimal points in our whole numbers. if value < 1: self.value = value else: self.value = int(value) self.state = 0 self.spr.type = 'bead' self.fade_level = 0 # Used for changing color. self.max_fade_level = max_fade self.tristate = tristate # Beads can be +/- or off. def hide(self): ''' Hide the sprite associated with the bead. ''' if self.spr is not None: self.spr.hide() def show(self): ''' Show the sprite associated with the bead. ''' if self.spr is not None: self.spr.set_layer(BEAD_LAYER) def move(self, offset, move_the_bead=True): ''' Generic move method: sets state and level. ''' if self.spr is None: return if move_the_bead: self.spr.move_relative((0, offset)) if not self.tristate: self.state = 1 - self.state elif self.state == 1: # moving bead back to center self.state = 0 elif self.state == -1: # moving bead back to center self.state = 0 else: # bead is in the center if offset > 0: # moving bead down to bottom self.state = -1 else: # moving bead up to top self.state = 1 self.set_fade_level(self.max_fade_level) self.update_label() def move_up(self): ''' Move a bead up as far as it will go. ''' self.move(-self.offset) def move_down(self): ''' Move a bead down as far as it will go. ''' self.move(self.offset) def get_value(self): ''' Return a value based upon bead state. ''' return self.state * self.value def get_state(self): ''' Is the bead active? ''' return self.state def set_color(self, color): ''' Set the color of the bead. ''' if self.spr is None: return self.spr.set_image(color) self.spr.inval() self.show() def set_label_color(self, color): ''' Set the label color for a bead (default is black). ''' if self.spr is None: return self.spr.set_label_color(color) def get_fade_level(self): ''' Return color fade level of bead. ''' return self.fade_level def set_fade_level(self, fade_level): ''' Set color fade level of bead. ''' self.fade_level = fade_level def update_label(self): ''' Label active beads. ''' if self.spr is None: return value = self.get_value() if self.state == 1 and value < 10000 and value > 0.05: value = self.get_value() if value < 1: self.spr.set_label(dec2frac(value)) else: self.spr.set_label(int(value)) elif self.state == -1 and value > -10000 and value < -0.05: value = self.get_value() if value > -1: self.spr.set_label('–' + dec2frac(-value)) else: self.spr.set_label(int(value)) else: self.spr.set_label('') class Rod(): ''' The Rod class is used to define a rod to hold beads. ''' def __init__(self, beads): ''' We store a sprite for each rod and allocate its beads. ''' self.spr = None self.label = None self.beads = beads def update(self, sprites, color, frame_height, i, x, y, scale, bead_count, cuisenaire=False, bead_color=None): self._bead_count = bead_count self.sprites = sprites rod = _svg_header(10, frame_height - (FRAME_STROKE_WIDTH * 2), scale) + \ _svg_rect(10, frame_height - (FRAME_STROKE_WIDTH * 2), 0, 0, 0, 0, color, '#404040') + \ _svg_footer() self.index = i self.scale = scale if self.spr is None: self.spr = Sprite(sprites, x, y, _svg_str_to_pixbuf(rod)) else: self.spr.set_image(_svg_str_to_pixbuf(rod)) self.spr.move((x, y)) self.spr.type = 'frame' self.lozenge = False self.white_beads = [] self.color_beads = [] if cuisenaire: # Bead size scaled to bead value bead_scale = 10.0 / (self.index + 1) self.lozenge = True else: bead_scale = 1.0 for i in range(MAX_FADE_LEVEL + 1): if bead_color is None: fade = _calc_fade('#FFFFFF', '#FFFF00', i, MAX_FADE_LEVEL) else: fade = _calc_fade(bead_color, '#FFFFFF', i, MAX_FADE_LEVEL) self.white_beads.append(_svg_str_to_pixbuf( _svg_header(BEAD_WIDTH, BEAD_HEIGHT, self.scale, stretch=bead_scale) + \ _svg_bead(fade, '#000000', stretch=bead_scale) + \ _svg_footer())) self.black_bead = _svg_str_to_pixbuf( _svg_header(BEAD_WIDTH, BEAD_HEIGHT, self.scale, stretch=bead_scale) + \ _svg_bead('#000000', '#000000', stretch=bead_scale) + \ _svg_footer()) for i in range(len(COLORS)): self.color_beads.append(_svg_str_to_pixbuf( _svg_header(BEAD_WIDTH, BEAD_HEIGHT, self.scale, stretch=bead_scale) + \ _svg_bead(COLORS[i], '#000000', stretch=bead_scale) + \ _svg_footer())) bo = (BEAD_WIDTH - BEAD_OFFSET) * self.scale / 2 ro = (BEAD_WIDTH + 5) * self.scale / 2 if self.label is None: self.label = Sprite(self.sprites, x - bo, y + self.spr.rect[3], _svg_str_to_pixbuf( _svg_header(BEAD_WIDTH, BEAD_HEIGHT, self.scale, stretch=1.0) + \ _svg_rect(BEAD_WIDTH, BEAD_HEIGHT, 0, 0, 0, 0, 'none', 'none') + \ _svg_footer())) else: self.label.move((x - bo, y + self.spr.rect[3])) self.label.type = 'frame' self.label.set_label_color('white') self.label.set_layer(MARK_LAYER) def allocate_beads(self, top_beads, bot_beads, top_factor, bead_value, bot_size, color=False, middle_black=False, all_black=False, tristate=False): ''' Beads get allocated per rod ''' self.top_beads = top_beads # number of beads above the bar self.bot_beads = bot_beads # number of beads below the bar # number of beads that could fit might not match number of beads # (e.g., Schety) self.bot_size = bot_size # top bead value == bead value * top factor self.top_factor = top_factor self.fade = False x = self.spr.rect[0] y = self.spr.rect[1] dx = (BEAD_WIDTH + BEAD_OFFSET) * self.scale bo = (BEAD_WIDTH - BEAD_OFFSET) * self.scale / 4 ro = (BEAD_WIDTH + 5) * self.scale / 2 # how far this bead moves bead_displacement = 2 * BEAD_HEIGHT * self.scale if color: bead_color = self.color_beads[self.index] elif all_black: bead_color = self.black_bead elif middle_black: # special patterning for Schety if bot_beads == self.bot_size: middle = [4, 5] else: middle = [1, 2] else: bead_color = self.white_beads[0] self.fade = True if self.fade: max_fade_level = MAX_FADE_LEVEL else: max_fade_level = 0 for i in range(top_beads): self.beads[i + self._bead_count].update( self.sprites, bead_color, bead_displacement, top_factor * bead_value, max_fade=max_fade_level) self.beads[i + self._bead_count].spr.move( (x - ro + bo, y + i * BEAD_HEIGHT * self.scale)) for i in range(bot_beads): displacement = bead_displacement if top_beads > 0: yy = y + (top_beads + 5 + i) * BEAD_HEIGHT * self.scale else: yy = y + (2 + i) * BEAD_HEIGHT * self.scale if all_black: bead_color = self.black_bead elif middle_black: if i in middle: bead_color = self.black_bead else: bead_color = self.white_beads[0] elif not color: bead_color = self.white_beads[0] # short row if not self.lozenge and (self.bot_beads < self.bot_size): offset = (self.bot_size - self.bot_beads) * BEAD_HEIGHT * \ self.scale yy += offset displacement += offset # center tristate beads vertically on the rod if tristate: if self.lozenge: offset = BEAD_HEIGHT * self.scale else: offset = (self.bot_size - self.bot_beads + 2) * \ BEAD_HEIGHT * self.scale / 2 yy -= offset displacement -= offset self.beads[i + self._bead_count + top_beads].update( self.sprites, bead_color, displacement, bead_value, tristate=tristate, max_fade=max_fade_level) self.beads[i + self._bead_count + top_beads].spr.move( (x - ro + bo, yy)) if bead_color == self.black_bead: self.beads[i + self._bead_count + top_beads].set_label_color( '#ffffff') # FIXME # Lozenged-shaped beads need to be spaced out more if self.beads[i + self._bead_count + top_beads].spr.rect[3] > \ BEAD_HEIGHT * self.scale: self.beads[i + self._bead_count + top_beads].spr.move_relative( (0, i * ( self.beads[i + self._bead_count + top_beads].spr.rect[3] - \ (BEAD_HEIGHT * self.scale)))) # self._bead_count -= (self.top_beads + self.bot_beads) if color: for i in range(self.top_beads + self.bot_beads): self.beads[i + self._bead_count].set_label_color( LABELS[self.index]) def hide(self): if self.spr is None: return for i in range(self.top_beads + self.bot_beads): self.beads[i + self._bead_count].hide() self.spr.hide() self.label.hide() def show(self): if self.spr is None: return for i in range(self.top_beads + self.bot_beads): self.beads[i + self._bead_count].show() self.spr.set_layer(ROD_LAYER) self.label.set_layer(MARK_LAYER) def get_max_value(self): ''' Returns maximum numeric value for this rod ''' if self.spr is None: return 0 max = 0 for i in range(self.top_beads + self.bot_beads): max += self.beads[i + self._bead_count].value return max def get_value(self): if self.spr is None: return 0 sum = 0 for i in range(self.top_beads + self.bot_beads): sum += self.beads[i + self._bead_count].get_value() return sum def get_bead_count(self): ''' Returns number of active bottom-bead equivalents on this rod''' if self.spr is None: return 0 count = 0 for i in range(self.top_beads + self.bot_beads): if self.beads[i + self._bead_count].get_state() == 1: if i < self.top_beads: count += self.top_factor else: count += 1 if self.beads[i + self._bead_count].get_state() == -1: count -= 1 return count def set_number(self, number): ''' Try to set a value equal to number; return any remainder ''' if self.spr is None: return 0 count = 0 for i in range(self.top_beads + self.bot_beads): if number >= self.beads[i + self._bead_count].value: number -= self.beads[i + self._bead_count].value if i < self.top_beads: count += self.top_factor else: count += 1 self.set_value(count) return number def set_value(self, value): ''' Move beads to represent a numeric value ''' if self.spr is None: return if self.top_beads > 0: bot = value % self.top_factor top = (value - bot) / self.top_factor else: bot = value top = 0 self.reset() # Set the top. for i in range(top): self.beads[self._bead_count + self.top_beads - i - 1].move_down() # Set the bottom for i in range(bot): self.beads[self._bead_count + self.top_beads + i].move_up() if value > 0: self.set_label(self.get_bead_count()) else: self.label.set_label('') def reset(self): if self.spr is None: return # Clear the top. for i in range(self.top_beads): if self.beads[i + self._bead_count].get_state() == 1: self.beads[i].move_up() # Clear the bottom. for i in range(self.bot_beads): if self.beads[self.top_beads + i + self._bead_count].get_state() \ == 1: self.beads[self.top_beads + i + self._bead_count].move_down() # Fade beads for i in range(self.top_beads + self.bot_beads): if self.beads[self._bead_count + i].fade_level > 0: self.beads[self._bead_count + i].fade_level = 0 self.beads[self._bead_count + i].set_color(self.white_beads[0]) self.label.set_label('') def fade_colors(self): ''' Reduce the saturation level of every bead. ''' if self.spr is None: return if self.fade: for i in range(self.top_beads + self.bot_beads): if self.beads[self._bead_count + i].get_fade_level() > 0: self.beads[self._bead_count + i].set_color( self.white_beads[self.beads[ self._bead_count + i].get_fade_level() - 1]) self.beads[self._bead_count + i].set_fade_level( self.beads[self._bead_count + i].get_fade_level() - 1) def move_bead(self, sprite, dy): ''' Move a bead (or beads) up or down a rod. ''' if self.spr is None: return False # Find the bead associated with the sprite. i = -1 for j in range(self.top_beads + self.bot_beads): if sprite == self.beads[self._bead_count + j].spr: i = j break if i == -1: return False if self.fade and self.beads[self._bead_count + i].max_fade_level > 0: self.beads[self._bead_count + i].set_color(self.white_beads[3]) if i < self.top_beads: if dy > 0 and self.beads[self._bead_count + i].get_state() == 0: self.beads[self._bead_count + i].move_down() # Make sure beads below this bead are also moved. for ii in range(self.top_beads - i): if self.beads[self._bead_count + i + ii].state == 0: if self.fade and \ self.beads[self._bead_count + i].max_fade_level > 0: self.beads[self._bead_count + i + ii].set_color( self.white_beads[3]) self.beads[self._bead_count + i + ii].move_down() elif dy < 0 and self.beads[self._bead_count + i].state == 1: self.beads[self._bead_count + i].move_up() # Make sure beads above this bead are also moved. for ii in range(i + 1): if self.beads[self._bead_count + i - ii].state == 1: if self.fade and \ self.beads[self._bead_count + i].max_fade_level > 0: self.beads[self._bead_count + i - ii].set_color( self.white_beads[3]) self.beads[self._bead_count + i - ii].move_up() else: if dy < 0 and self.beads[self._bead_count + i].state == 0: self.beads[self._bead_count + i].move_up() # Make sure beads above this bead are also moved. for ii in range(i - self.top_beads + 1): if self.beads[self._bead_count + i - ii].state == 0: if self.fade and \ self.beads[self._bead_count + i].max_fade_level > 0: self.beads[self._bead_count + i - ii].set_color( self.white_beads[3]) self.beads[self._bead_count + i - ii].move_up() elif dy < 0 and self.beads[self._bead_count + i].state == -1: self.beads[self._bead_count + i].move_up() for ii in range(i - self.top_beads + 1): if self.beads[self._bead_count + i - ii].state == -1: if self.fade and \ self.beads[self._bead_count + i].max_fade_level > 0: self.beads[self._bead_count + i - ii].set_color( self.white_beads[3]) self.beads[self._bead_count + i - ii].move_up() elif dy > 0 and self.beads[self._bead_count + i].state == 1: self.beads[self._bead_count + i].move_down() # Make sure beads below this bead are also moved. for ii in range(self.top_beads + self.bot_beads - i): if self.beads[self._bead_count + i + ii].state == 1: if self.fade and \ self.beads[self._bead_count + i].max_fade_level > 0: self.beads[self._bead_count + i + ii].set_color( self.white_beads[3]) self.beads[self._bead_count + i + ii].move_down() elif dy > 0 and self.beads[self._bead_count + i].state == 0 and \ self.beads[self._bead_count + i].tristate: self.beads[self._bead_count + i].move_down() # Make sure beads below this bead are also moved. for ii in range(self.top_beads + self.bot_beads - i): if self.beads[self._bead_count + i + ii].state == 0: if self.fade and \ self.beads[self._bead_count + i].max_fade_level > 0: self.beads[self._bead_count + i + ii].set_color( self.white_beads[3]) self.beads[self._bead_count + i + ii].move_down() self.set_label(self.get_bead_count()) def set_label(self, n): ''' Different abaci use different labeling schemes. ''' if self.spr is None: return # Use hex notation on hex abacus if self.top_beads == 1 and self.bot_beads == 7 and \ self.top_factor == 8: self.label.set_label('%x' % n) # Only show 0 on binary abacus elif n == 0 and not (self.top_beads == 0 and self.bot_beads == 1): self.label.set_label('') else: self.label.set_label(n) return class Abacus(): ''' The Abacus class is used to define the user interaction. ''' def __init__(self, canvas, parent=None): ''' Initialize the canvas and set up the callbacks. ''' self.activity = parent if parent is None: # Starting from command line self.sugar = False self.canvas = canvas self.bead_colors = ['#FFFFFF', '#FFFFFF'] else: # Starting from Sugar self.sugar = True self.canvas = canvas self.bead_colors = parent.bead_colors parent.show_all() self.canvas.set_can_focus(True) 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.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('motion-notify-event', self._mouse_move_cb) self.canvas.connect('key_press_event', self._keypress_cb) self.width = Gdk.Screen.width() self.height = Gdk.Screen.height() - GRID_CELL_SIZE self.sprites = Sprites(self.canvas) self.scale = 1.33 * Gdk.Screen.height() / 900.0 self.dragpos = 0 self.press = None self.last = None locale.setlocale(locale.LC_NUMERIC, '') self.decimal_point = locale.localeconv()['decimal_point'] if self.decimal_point == '' or self.decimal_point is None: self.decimal_point = '.' background_svg = _svg_header(self.width, self.height, 1.0) + \ _svg_rect(self.width, self.height, 0, 0, 0, 0, '#FFFFFF', '#FFFFFF') + \ _svg_footer() background = Sprite(self.sprites, 0, 0, _svg_str_to_pixbuf(background_svg)) background.set_layer(1) self.decimal = None self.japanese = None self.chinese = None self.mayan = None self.hex = None self.binary = None self.russian = None self.fraction = None self.caacupe = None self.cuisenaire = None self.custom = None self.mode_dict = {'decimal': [self.decimal, Decimal], 'soroban': [self.japanese, Soroban], 'suanpan': [self.chinese, Suanpan], 'nepohualtzintzin': [self.mayan, Nepohualtzintzin], 'hexadecimal': [self.hex, Hex], 'binary': [self.binary, Binary], 'schety': [self.russian, Schety], 'fraction': [self.fraction, Fractions], 'caacupe': [self.caacupe, Caacupe], 'cuisenaire': [self.cuisenaire, Cuisenaire], 'custom': [self.custom, Custom] } self.bead_cache = [] for i in range(MAX_BEADS): self.bead_cache.append(Bead()) self.rod_cache = [] for i in range(MAX_BEADS): self.rod_cache.append(Rod(self.bead_cache)) self.chinese = Suanpan(self, self.bead_colors) self.mode = self.chinese self.mode.show() def select_abacus(self, abacus): self.mode.hide() if self.mode_dict[abacus][0] is None: self.mode_dict[abacus][0] = self.mode_dict[abacus][1]( self, self.bead_colors) else: self.mode_dict[abacus][0].draw_rods_and_beads() self.mode = self.mode_dict[abacus][0] self.mode.show() self.mode.label(self.generate_label()) 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)) self.last = self.press if self.press is not None: if self.press.type == 'bead': self.dragpos = y elif self.press.type == 'mark': self.dragpos = x elif self.press == self.mode.label_bar: self.mode.label(self.generate_label(sum_only=True) + CURSOR) number = self.press.labels[0].replace(CURSOR, '') if '/' in number: # need to convert to decimal form try: userdefined = {} exec 'def f(): return ' + number.replace(' ', '+') + \ '.' in globals(), userdefined value = userdefined.values()[0]() number = str(value) self.press.set_label( number.replace('.', self.decimal_point) + CURSOR) except: traceback.print_exc() number = '' self.press = None else: self.press = None return True def _mouse_move_cb(self, win, event): ''' Callback to handle the mouse moves ''' if self.press is None: self.dragpos = 0 return True win.grab_focus() x, y = map(int, event.get_coords()) if self.press.type == 'mark': mx, my = self.mode.mark.get_xy() self.mode.move_mark(x - mx) return True def _button_release_cb(self, win, event): ''' Callback to handle the button releases ''' if self.press is None: self.dragpos = 0 return True win.grab_focus() x, y = map(int, event.get_coords()) if self.press.type == 'bead': self.mode.move_bead(self.press, y - self.dragpos) self.press = None self.mode.label(self.generate_label()) return True def _keypress_cb(self, area, event): ''' Keypress: moving the slides with the arrow keys ''' k = Gdk.keyval_name(event.keyval) if k in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'period', 'minus', 'Return', 'BackSpace', 'comma']: if self.last == self.mode.label_bar: self._process_numeric_input(self.last, k) elif k == 'r': self.mode.reset_abacus() return True def _process_numeric_input(self, sprite, keyname): ''' Make sure numeric input is valid. ''' oldnum = sprite.labels[0].replace(CURSOR, '') newnum = oldnum if len(oldnum) == 0: oldnum = '0' if keyname == 'minus': if oldnum == '0': newnum = '-' elif oldnum[0] != '-': newnum = '-' + oldnum else: newnum = oldnum elif keyname == 'comma' and self.decimal_point == ',' and \ ',' not in oldnum: newnum = oldnum + ',' elif keyname == 'period' and self.decimal_point == '.' and \ '.' not in oldnum: newnum = oldnum + '.' elif keyname == 'BackSpace': if len(oldnum) > 0: newnum = oldnum[:len(oldnum) - 1] else: newnum = '' elif keyname in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']: if oldnum == '0': newnum = keyname else: newnum = oldnum + keyname elif keyname == 'Return': self.mode.reset_abacus() self.mode.set_value_from_number( float(newnum.replace(self.decimal_point, '.'))) self.mode.label(self.generate_label()) return else: newnum = oldnum if newnum == '.': newnum = '0.' if len(newnum) > 0 and newnum != '-': try: float(newnum.replace(self.decimal_point, '.')) except ValueError, e: newnum = oldnum sprite.set_label(newnum + CURSOR) # Handle the expose-event by drawing 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() def generate_label(self, sum_only=False): ''' The complexity below is to make the label as simple as possible ''' rod_sums = '' multiple_rods = False for x in self.mode.get_rod_values(): if x > 0: if x > 0.005: rod_value = dec2frac(x) else: rod_value = str(x) if rod_sums == '': rod_sums = rod_value else: multiple_rods = True rod_sums += ' + %s' % (rod_value) elif x < 0: if x < 0.005: rod_value = dec2frac(-x) else: rod_value = str(-x) if rod_sums == '': rod_sums = '–%s' % (rod_value) else: multiple_rods = True rod_sums += ' – %s' % (rod_value) if rod_sums == '': return '' else: abacus_value = float(self.mode.value()) if abacus_value == 0: value = '0' elif abacus_value > 0: whole = int(floor(abacus_value)) fraction = abacus_value - whole if whole == 0: if fraction > 0.005: value = dec2frac(fraction) else: value = str(fraction) elif fraction == 0: value = '%d' % (whole) else: if fraction > 0.005: value = '%d %s' % (whole, dec2frac(fraction)) else: value = '%d %s' % (whole, str(fraction)) else: whole = int(ceil(abacus_value)) fraction = abacus_value - whole if whole == 0: if fraction < 0.005: value = '–%s' % (dec2frac(-fraction)) else: value = '–%s' % (str(-fraction)) elif fraction == 0: value = '–%d' % (-whole) else: if fraction < 0.005: value = '–%d %s' % (-whole, dec2frac(-fraction)) else: value = '–%d %s' % (-whole, str(-fraction)) if value == '' or value == '–': value = '0' if multiple_rods and not sum_only: return rod_sums + ' = ' + value else: return value class AbacusGeneric(): ''' A generic abacus: a frame, rods, and beads. ''' def __init__(self, abacus, bead_colors=None): ''' Specify parameters that define the abacus ''' self.abacus = abacus self.bead_colors = bead_colors self.set_parameters() self.create() def set_parameters(self, rods=15, top=2, bot=5, factor=5, base=10): ''' Define the physical paramters. ''' self.name = 'suanpan' self.num_rods = rods self.bot_beads = top self.top_beads = bot self.top_factor = factor self.base = base def create(self, dots=False): ''' Create and position the sprites that compose the abacus ''' # Width is a function of the number of rods self.frame_width = self.num_rods * (BEAD_WIDTH + BEAD_OFFSET) + \ FRAME_STROKE_WIDTH * 2 # Height is a function of the number of beads if self.top_beads > 0: self.frame_height = (self.bot_beads + self.top_beads + 5) * \ BEAD_HEIGHT + FRAME_STROKE_WIDTH * 2 else: self.frame_height = (self.bot_beads + 2) * BEAD_HEIGHT + \ FRAME_STROKE_WIDTH * 2 # Draw the frame... x = (self.abacus.width - (self.frame_width * self.abacus.scale)) / 2 y = int(BEAD_HEIGHT * 1.5) frame = _svg_header(self.frame_width, self.frame_height, self.abacus.scale) + \ _svg_rect(self.frame_width, self.frame_height, FRAME_STROKE_WIDTH / 2, FRAME_STROKE_WIDTH / 2, 0, 0, '#000000', '#000000') + \ _svg_rect(self.frame_width - \ (FRAME_STROKE_WIDTH * 2), self.frame_height - \ (FRAME_STROKE_WIDTH * 2), 0, 0, FRAME_STROKE_WIDTH, FRAME_STROKE_WIDTH, '#C0C0C0', '#000000') \ + \ _svg_footer() self.frame = Sprite(self.abacus.sprites, x, y, _svg_str_to_pixbuf(frame)) self.frame.type = 'frame' # Some abaci (Soroban) use a dot to show the units position if dots: dx = (BEAD_WIDTH + BEAD_OFFSET) * self.abacus.scale dotx = int(self.abacus.width / 2) - 5 doty = [y + 5, y + self.frame.rect[3] - 15] self.dots = [] white_dot = _svg_header(10, 10, self.abacus.scale) + \ _svg_circle(5, 5, 5, '#FFFFFF', '#000000') + \ _svg_footer() self.dots.append(Sprite(self.abacus.sprites, dotx, doty[0], _svg_str_to_pixbuf(white_dot))) self.dots.append(Sprite(self.abacus.sprites, dotx, doty[1], _svg_str_to_pixbuf(white_dot))) black_dot = _svg_header(10, 10, self.abacus.scale) + \ _svg_circle(5, 5, 5, '#282828', '#FFFFFF') + \ _svg_footer() for i in range(int(self.num_rods / 4 - 1)): # mark 1000s if i % 2 == 0: dot = black_dot else: dot = white_dot self.dots.append(Sprite(self.abacus.sprites, dotx - 3 * (i + 1) * dx, doty[0], _svg_str_to_pixbuf(dot))) self.dots.append(Sprite(self.abacus.sprites, dotx + 3 * (i + 1) * dx, doty[0], _svg_str_to_pixbuf(dot))) self.dots.append(Sprite(self.abacus.sprites, dotx - 3 * (i + 1) * dx, doty[1], _svg_str_to_pixbuf(dot))) self.dots.append(Sprite(self.abacus.sprites, dotx + 3 * (i + 1) * dx, doty[1], _svg_str_to_pixbuf(dot))) for dot in self.dots: dot.set_layer(DOT_LAYER) dot.type = 'frame' # Draw the label bar label = _svg_header(self.frame_width, BEAD_HEIGHT, self.abacus.scale) + \ _svg_rect(self.frame_width, BEAD_HEIGHT, 0, 0, 0, 0, 'none', 'none') + \ _svg_footer() self.label_bar = Sprite(self.abacus.sprites, x, 0, _svg_str_to_pixbuf(label)) self.label_bar.type = 'frame' self.label_bar.set_label_attributes(12, rescale=False) self.label_bar.set_label_color('black') # and then the rods and beads. x += FRAME_STROKE_WIDTH * self.abacus.scale y += FRAME_STROKE_WIDTH * self.abacus.scale self.rods = self.abacus.rod_cache self.draw_rods_and_beads(x, y) # Draw the dividing bar... bar = _svg_header(self.frame_width - (FRAME_STROKE_WIDTH * 2), BEAD_HEIGHT, self.abacus.scale) + \ _svg_rect(self.frame_width - (FRAME_STROKE_WIDTH * 2), BEAD_HEIGHT, 0, 0, 0, 0, '#000000', '#000000') + \ _svg_footer() if self.top_beads > 0: self.bar = Sprite(self.abacus.sprites, x, y + (self.top_beads + 2) * BEAD_HEIGHT * \ self.abacus.scale, _svg_str_to_pixbuf(bar)) else: self.bar = Sprite(self.abacus.sprites, x, y - FRAME_STROKE_WIDTH * self.abacus.scale, _svg_str_to_pixbuf(bar)) self.bar.type = 'frame' # and finally, the mark. mark = _svg_header(20, 15, self.abacus.scale) + \ _svg_indicator() + \ _svg_footer() dx = (BEAD_WIDTH + BEAD_OFFSET) * self.abacus.scale self.mark = Sprite(self.abacus.sprites, x + (self.num_rods - 1) * dx, y - (FRAME_STROKE_WIDTH / 2) * self.abacus.scale, _svg_str_to_pixbuf(mark)) self.mark.type = 'mark' def draw_rods_and_beads(self, x=None, y=None): ''' Draw the rods and beads ''' if x is None: x = self.rod_x y = self.rod_y else: self.rod_x = x self.rod_y = y dx = (BEAD_WIDTH + BEAD_OFFSET) * self.abacus.scale ro = (BEAD_WIDTH + 5) * self.abacus.scale / 2 bead_count = 0 for i in range(self.num_rods): if self.bead_colors is not None: bead_color = self.bead_colors[i % 2] else: bead_color = None bead_value = pow(self.base, self.num_rods - i - 1) self.rods[i].update(self.abacus.sprites, ROD_COLORS[i % len(ROD_COLORS)], self.frame_height, i, x + i * dx + ro, y, self.abacus.scale, bead_count, bead_color=bead_color) bead_count += (self.top_beads + self.bot_beads) self.rods[i].allocate_beads(self.top_beads, self.bot_beads, self.top_factor, bead_value, self.bot_beads) def hide(self): ''' Hide the rod, beads, mark, and frame. ''' for i in range(self.num_rods): self.rods[i].hide() self.bar.hide() self.label_bar.hide() self.frame.hide() self.mark.hide() if hasattr(self, 'dots'): for dot in self.dots: dot.hide() def show(self): ''' Show the rod, beads, mark, and frame. ''' self.frame.set_layer(FRAME_LAYER) for i in range(self.num_rods): self.rods[i].show() self.bar.set_layer(BAR_LAYER) self.label_bar.set_layer(BAR_LAYER) if hasattr(self, 'dots'): for dot in self.dots: dot.set_layer(DOT_LAYER) self.mark.set_layer(MARK_LAYER) def set_value(self, string): ''' Set abacus to value in string ''' value = string.split() # Move the beads to correspond to column values. try: for i in range(self.num_rods): self.rods[i].set_value(int(value[i])) except IndexError: _logger.debug('bad saved string length %s (%d != 2 * %d)', string, len(string), self.num_rods) except ValueError: _logger.debug('bad saved string type %s (%s)', string, str(value[i])) def max_value(self): ''' Maximum value possible on abacus ''' max = 0 for i in range(self.num_rods): max += self.rods[i].get_max_value() return max def set_value_from_number(self, number): ''' Set abacus to value in string ''' self.reset_abacus() if number <= self.max_value(): for i in range(self.num_rods): number = self.rods[i].set_number(number) if number == 0: break def reset_abacus(self): ''' Reset beads to original position ''' for i in range(self.num_rods): self.rods[i].reset() def value(self, count_beads=False): ''' Return a string representing the value of each rod. ''' if count_beads: # Save the value associated with each rod as a 2-byte integer. string = '' value = [] for i in range(self.num_rods + 1): # +1 for overflow value.append(0) # Tally the values on each rod. for i in range(self.num_rods): value[i + 1] = self.rods[i].get_bead_count() # Save the value associated with each rod as a 2-byte integer. for i in value[1:]: string += '%2d ' % (i) else: rod_sum = 0 for i in range(self.num_rods): rod_sum += self.rods[i].get_value() string = str(rod_sum) return(string) def label(self, string): ''' Label with the string. (Used with self.value) ''' self.label_bar.set_label(string) def move_mark(self, dx): ''' Move indicator horizontally across the top of the frame. ''' self.mark.move_relative((dx, 0)) def fade_colors(self): ''' Reduce the saturation level of every bead. ''' for i in range(self.num_rods): self.rods[i].fade_colors() def move_bead(self, sprite, dy): ''' Move a bead (or beads) up or down a rod. ''' self.fade_colors() for i in range(self.num_rods): if self.rods[i].move_bead(sprite, dy): break def get_rod_values(self): ''' Return the sum of the values per rod as an array ''' v = [0] * (self.num_rods + 1) for i in range(self.num_rods): v[i + 1] = self.rods[i].get_value() return v[1:] class Custom(AbacusGeneric): ''' A custom-made abacus ''' def __init__(self, abacus, bead_colors=None): ''' Specify parameters that define the abacus ''' self.abacus = abacus self.name = 'custom' self.num_rods = 15 self.bot_beads = 5 self.top_beads = 2 self.top_factor = 5 self.base = 10 self.bead_colors = bead_colors def set_custom_parameters(self, rods=15, top=2, bot=5, factor=5, base=10): ''' Specify parameters that define the abacus ''' self.name = 'custom' self.num_rods = rods self.bot_beads = bot self.top_beads = top self.top_factor = factor self.base = base class Nepohualtzintzin(AbacusGeneric): ''' A Mayan abacus ''' def __init__(self, abacus, bead_colors=None): ''' Specify parameters that define the abacus ''' self.abacus = abacus self.bead_colors = bead_colors self.set_parameters() self.create() def set_parameters(self): ''' Specify parameters that define the abacus ''' self.name = 'nepohualtzintzin' self.num_rods = 13 self.bot_beads = 4 self.top_beads = 3 self.base = 20 self.top_factor = 5 class Suanpan(AbacusGeneric): ''' A Chinese abacus ''' def __init__(self, abacus, bead_colors=None): ''' Specify parameters that define the abacus ''' self.abacus = abacus self.bead_colors = bead_colors self.set_parameters() self.create() def set_parameters(self): ''' Create a Chinese abacus: 15 by (5,2). ''' self.name = 'suanpan' self.num_rods = 15 self.bot_beads = 5 self.top_beads = 2 self.base = 10 self.top_factor = 5 class Soroban(AbacusGeneric): ''' A Japanese abacus ''' def __init__(self, abacus, bead_colors=None): ''' Specify parameters that define the abacus ''' self.abacus = abacus self.bead_colors = bead_colors self.set_parameters() self.create(dots=True) def set_parameters(self): ''' create a Japanese abacus: 15 by (4,1) ''' self.name = 'soroban' self.num_rods = 15 self.bot_beads = 4 self.top_beads = 1 self.base = 10 self.top_factor = 5 def draw_rods_and_beads(self, x=None, y=None): ''' Draw the rods and beads: units offset to center''' if x is None: x = self.rod_x y = self.rod_y else: self.rod_x = x self.rod_y = y dx = (BEAD_WIDTH + BEAD_OFFSET) * self.abacus.scale ro = (BEAD_WIDTH + 5) * self.abacus.scale / 2 bead_count = 0 for i in range(self.num_rods): if self.bead_colors is not None: bead_color = self.bead_colors[i % 2] else: bead_color = None bead_value = pow(self.base, int(self.num_rods / 2) - i) self.rods[i].update(self.abacus.sprites, ROD_COLORS[i % len(ROD_COLORS)], self.frame_height, i, x + i * dx + ro, y, self.abacus.scale, bead_count, bead_color=bead_color) bead_count += (self.top_beads + self.bot_beads) self.rods[i].allocate_beads(self.top_beads, self.bot_beads, self.top_factor, bead_value, self.bot_beads) class Hex(AbacusGeneric): ''' A hexadecimal abacus ''' def __init__(self, abacus, bead_colors=None): ''' Specify parameters that define the abacus ''' self.abacus = abacus self.bead_colors = bead_colors self.set_parameters() self.create() def set_parameters(self): ''' create a hexadecimal abacus: 15 by (7,1) ''' self.name = 'hexadecimal' self.num_rods = 15 self.bot_beads = 7 self.top_beads = 1 self.base = 16 self.top_factor = 8 class Decimal(AbacusGeneric): ''' A decimal abacus ''' def __init__(self, abacus, bead_colors=None): ''' Specify parameters that define the abacus ''' self.abacus = abacus self.bead_colors = bead_colors self.set_parameters() self.create() def set_parameters(self): ''' create a decimal abacus: 10 by (10,0) ''' self.name = 'decimal' self.num_rods = 10 self.bot_beads = 10 self.top_beads = 0 self.base = 10 self.top_factor = 1 def draw_rods_and_beads(self, x=None, y=None): ''' Draw the rods and beads: override bead color''' if x is None: x = self.rod_x y = self.rod_y else: self.rod_x = x self.rod_y = y dx = (BEAD_WIDTH + BEAD_OFFSET) * self.abacus.scale ro = (BEAD_WIDTH + 5) * self.abacus.scale / 2 bead_count = 0 for i in range(self.num_rods): if self.bead_colors is not None: bead_color = self.bead_colors[i % 2] else: bead_color = None bead_value = pow(self.base, self.num_rods - i - 1) self.rods[i].update(self.abacus.sprites, '#404040', self.frame_height, i, x + i * dx + ro, y, self.abacus.scale, bead_count, bead_color=bead_color) bead_count += (self.top_beads + self.bot_beads) self.rods[i].allocate_beads(self.top_beads, self.bot_beads, self.top_factor, bead_value, self.bot_beads, color=True) class Binary(AbacusGeneric): ''' A binary abacus ''' def __init__(self, abacus, bead_colors=None): ''' Specify parameters that define the abacus ''' self.abacus = abacus self.bead_colors = bead_colors self.set_parameters() self.create() def set_parameters(self): ''' create a Binary abacus: 15 by (1,0) ''' self.name = 'binary' self.num_rods = 15 self.bot_beads = 1 self.top_beads = 0 self.base = 2 self.top_factor = 1 class Schety(AbacusGeneric): ''' A Russian abacus ''' def __init__(self, abacus, bead_colors=None): ''' Specify parameters that define the abacus ''' self.abacus = abacus self.bead_colors = bead_colors self.set_parameters() self.create() def set_parameters(self): ''' Create a Russian abacus: 15 by 10 (with one rod of 4 beads). ''' self.name = 'schety' self.top_beads = 0 self.bot_beads = 10 self.bead_count = (10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 4, 10, 10, 10, 10) self.bead_value = (10 ** 9, 10 ** 8, 10 ** 7, 10 ** 6, 10 ** 5, 10000, 1000, 100, 10, 1, .25, .1, .01, .001, .0001) self.num_rods = len(self.bead_count) self.base = 10 self.top_factor = 1 def draw_rods_and_beads(self, x=None, y=None): ''' Draw the rods and beads: short column for 1/4 ''' if x is None: x = self.rod_x y = self.rod_y else: self.rod_x = x self.rod_y = y dx = (BEAD_WIDTH + BEAD_OFFSET) * self.abacus.scale ro = (BEAD_WIDTH + 5) * self.abacus.scale / 2 bead_count = 0 for i in range(self.num_rods): if self.bead_colors is not None: bead_color = self.bead_colors[i % 2] else: bead_color = None self.rods[i].update(self.abacus.sprites, '#404040', self.frame_height, i, x + i * dx + ro, y, self.abacus.scale, bead_count, bead_color=bead_color) bead_count += (self.top_beads + self.bot_beads) self.rods[i].allocate_beads(self.top_beads, self.bead_count[i], self.top_factor, self.bead_value[i], self.bead_count[-1], middle_black=True) class Fractions(Schety): ''' Inherit from Russian abacus. ''' def __init__(self, abacus, bead_colors=None): ''' Specify parameters that define the abacus ''' self.abacus = abacus self.bead_colors = bead_colors self.set_parameters() self.create() def set_parameters(self): ''' Create an abacus with fractions: 15 by 10 (with 1/2, 1/3. 1/4, 1/5, 1/6, 1/8, 1/9, 1/10, 1/12). ''' self.bead_count = (10, 10, 10, 10, 10, 10, 2, 3, 4, 5, 6, 8, 9, 10, 12) self.bead_value = (100000, 10000, 1000, 100, 10, 1, .5, 1 / 3., .25, .2, 1 / 6., .125, 1 / 9., .1, 1 / 12.) self.name = 'fraction' self.num_rods = 15 self.top_beads = 0 self.bot_beads = 12 self.base = 10 self.top_factor = 1 def draw_rods_and_beads(self, x=None, y=None): ''' Draw the rods and beads: short columns for fractions ''' if x is None: x = self.rod_x y = self.rod_y else: self.rod_x = x self.rod_y = y dx = (BEAD_WIDTH + BEAD_OFFSET) * self.abacus.scale ro = (BEAD_WIDTH + 5) * self.abacus.scale / 2 bead_count = 0 for i in range(self.num_rods): if self.bead_colors is not None: bead_color = self.bead_colors[i % 2] else: bead_color = None self.rods[i].update(self.abacus.sprites, '#404040', self.frame_height, i, x + i * dx + ro, y, self.abacus.scale, bead_count, bead_color=bead_color) bead_count += (self.top_beads + self.bot_beads) if i < 6: all_black = False else: all_black = True self.rods[i].allocate_beads(self.top_beads, self.bead_count[i], self.top_factor, self.bead_value[i], self.bead_count[-1], all_black=all_black) class Caacupe(Fractions): ''' Inherit from Fraction abacus. ''' def __init__(self, abacus, bead_colors=None): ''' Specify parameters that define the abacus ''' self.abacus = abacus self.bead_colors = bead_colors self.set_parameters() self.create() def set_parameters(self): ''' Create an abacus with fractions: 15 by 10 (with 1/2, 1/3. 1/4, 1/5, 1/6, 1/8, 1/9, 1/10, 1/12). ''' self.bead_count = (10, 10, 10, 10, 10, 10, 2, 3, 4, 5, 6, 8, 9, 10, 12) self.bead_value = (100000, 10000, 1000, 100, 10, 1, .5, 1 / 3., .25, .2, 1 / 6., .125, 1 / 9., .1, 1 / 12.) self.name = 'caacupe' self.num_rods = 15 self.top_beads = 0 self.bot_beads = 12 self.base = 10 self.top_factor = 1 def draw_rods_and_beads(self, x=None, y=None): ''' Draw the rods and beads: short columns for fractions ''' if x is None: x = self.rod_x y = self.rod_y else: self.rod_x = x self.rod_y = y dx = (BEAD_WIDTH + BEAD_OFFSET) * self.abacus.scale ro = (BEAD_WIDTH + 5) * self.abacus.scale / 2 bead_count = 0 for i in range(self.num_rods): if self.bead_colors is not None: bead_color = self.bead_colors[i % 2] else: bead_color = None self.rods[i].update(self.abacus.sprites, '#404040', self.frame_height, i, x + i * dx + ro, y, self.abacus.scale, bead_count, bead_color=bead_color) bead_count += (self.top_beads + self.bot_beads) if i < 6: all_black = False else: all_black = True self.rods[i].allocate_beads(self.top_beads, self.bead_count[i], self.top_factor, self.bead_value[i], self.bead_count[-1], all_black=all_black, tristate=True) class Cuisenaire(Caacupe): ''' Inherit from Caacupe abacus. ''' def __init__(self, abacus, bead_colors=None): ''' Specify parameters that define the abacus ''' self.abacus = abacus self.bead_colors = bead_colors self.set_parameters() self.create() def set_parameters(self): ''' Create an abacus with fractions: 10 by 10 (with 1/1, 1/2, 1/3. 1/4, 1/5, 1/6, 1/7, 1/8, 1/9, 1/10). ''' self.bead_count = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) self.bead_value = (1, .5, 1 / 3., .25, .2, 1 / 6., 1 / 7., .125, 1 / 9., .1) self.name = 'cuisenaire' self.num_rods = 10 self.top_beads = 0 self.bot_beads = 10 self.base = 10 self.top_factor = 1 def draw_rods_and_beads(self, x=None, y=None): ''' Draw the rods and beads: short columns for fractions ''' if x is None: x = self.rod_x y = self.rod_y else: self.rod_x = x self.rod_y = y dx = (BEAD_WIDTH + BEAD_OFFSET) * self.abacus.scale ro = (BEAD_WIDTH + 5) * self.abacus.scale / 2 bead_count = 0 for i in range(self.num_rods): if self.bead_colors is not None: bead_color = self.bead_colors[i % 2] else: bead_color = None self.rods[i].update(self.abacus.sprites, '#404040', self.frame_height, i, x + i * dx + ro, y, self.abacus.scale, bead_count, cuisenaire=True, bead_color=bead_color) bead_count += (self.top_beads + self.bot_beads) self.rods[i].allocate_beads(self.top_beads, self.bead_count[i], self.top_factor, self.bead_value[i], self.bead_count[-1], color=True, tristate=True)