#Copyright (c) 2007-8, Playful Invention Company. #Copyright (c) 2008-10, Walter Bender #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in #all copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN #THE SOFTWARE. import gtk import pickle import subprocess import dbus try: OLD_SUGAR_SYSTEM = False import json json.dumps from json import load as jload from json import dump as jdump except (ImportError, AttributeError): try: import simplejson as json from simplejson import load as jload from simplejson import dump as jdump except: OLD_SUGAR_SYSTEM = True from taconstants import HIDE_LAYER, COLLAPSIBLE, BLOCK_LAYER, HIT_HIDE, \ HIT_SHOW, XO1, XO15, UNKNOWN from StringIO import StringIO import os.path import logging _logger = logging.getLogger('turtleart-activity') def debug_output(message_string, running_sugar=False): """ unified debugging output """ if running_sugar: _logger.debug(message_string) else: print(message_string) def error_output(message_string, running_sugar=False): """ unified debugging output """ if running_sugar: _logger.error(message_string) else: print(message_string) class pythonerror(Exception): def __init__(self, value): self.value = value def __str__(self): return repr(self.value) def convert(x, fn, try_ord=True): ''' The strategy for mixing numbers and strings is to first try converting the string to a float; then if the string is a single character, try converting it to an ord; finally, just treat it as a string. Numbers appended to strings are first treated as ints, then floats. ''' try: return fn(x) except ValueError: if try_ord: xx, flag = chr_to_ord(x) if flag: return fn(xx) return x def chr_to_ord(x): """ Try to comvert a string to an ord """ if strtype(x) and len(x) == 1: try: return ord(x[0]), True except ValueError: return x, False return x, False def strtype(x): """ Is x a string type? """ if type(x) == str: return True if type(x) == unicode: return True return False def magnitude(pos): """ Calculate the magnitude of the distance between to blocks. """ x, y = pos return x * x + y * y def json_load(text): """ Load JSON data using what ever resources are available. """ if OLD_SUGAR_SYSTEM is True: _listdata = json.read(text) else: # strip out leading and trailing whitespace, nulls, and newlines clean_text = text.lstrip() clean_text = clean_text.replace('\12', '') clean_text = clean_text.replace('\00', '') _io = StringIO(clean_text.rstrip()) try: _listdata = jload(_io) except ValueError: # assume that text is ascii list _listdata = text.split() for i, value in enumerate(_listdata): _listdata[i] = convert(value, float) # json converts tuples to lists, so we need to convert back, return _tuplify(_listdata) def _tuplify(tup): """ Convert to tuples """ if type(tup) is not list: return tup return tuple(map(_tuplify, tup)) def get_id(connection): """ Get a connection block ID. """ if connection is not None and hasattr(connection, 'id'): return connection.id return None def json_dump(data): """ Save data using available JSON tools. """ if OLD_SUGAR_SYSTEM is True: return json.write(data) else: _io = StringIO() jdump(data, _io) return _io.getvalue() def get_load_name(suffix, load_save_folder): """ Open a load file dialog. """ _dialog = gtk.FileChooserDialog("Load...", None, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) _dialog.set_default_response(gtk.RESPONSE_OK) return do_dialog(_dialog, suffix, load_save_folder) def get_save_name(suffix, load_save_folder, save_file_name): """ Open a save file dialog. """ _dialog = gtk.FileChooserDialog("Save...", None, gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)) _dialog.set_default_response(gtk.RESPONSE_OK) if save_file_name is not None: _dialog.set_current_name(save_file_name + suffix) return do_dialog(_dialog, suffix, load_save_folder) def chooser(parent_window, filter, action): """ Choose an object from the datastore and take some action """ from sugar.graphics.objectchooser import ObjectChooser _chooser = None try: _chooser = ObjectChooser(parent=parent_window, what_filter=filter) except TypeError: _chooser = ObjectChooser(None, parent_window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT) if _chooser is not None: try: result = _chooser.run() if result == gtk.RESPONSE_ACCEPT: dsobject = _chooser.get_selected_object() action(dsobject) dsobject.destroy() finally: _chooser.destroy() del _chooser def data_from_file(ta_file): """ Open the .ta file, ignoring any .png file that might be present. """ file_handle = open(ta_file, "r") # # We try to maintain read-compatibility with all versions of Turtle Art. # Try pickle first; then different versions of json. # try: _data = pickle.load(file_handle) except: # Rewind necessary because of failed pickle.load attempt file_handle.seek(0) _text = file_handle.read() _data = data_from_string(_text) file_handle.close() return _data def data_from_string(text): """ JSON load data from a string. """ return json_load(text.replace(']],\n', ']], ')) def data_to_file(data, ta_file): """ Write data to a file. """ file_handle = file(ta_file, "w") file_handle.write(data_to_string(data)) file_handle.close() def data_to_string(data): """ JSON dump a string. """ return json_dump(data).replace(']], ', ']],\n') def do_dialog(dialog, suffix, load_save_folder): """ Open a file dialog. """ _result = None file_filter = gtk.FileFilter() file_filter.add_pattern('*' + suffix) file_filter.set_name("Turtle Art") dialog.add_filter(file_filter) dialog.set_current_folder(load_save_folder) _response = dialog.run() if _response == gtk.RESPONSE_OK: _result = dialog.get_filename() load_save_folder = dialog.get_current_folder() dialog.destroy() return _result, load_save_folder def save_picture(canvas, file_name=''): """ Save the canvas to a file. """ _pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, canvas.width, canvas.height) _pixbuf.get_from_drawable(canvas.canvas.images[0], canvas.canvas.images[0].get_colormap(), 0, 0, 0, 0, canvas.width, canvas.height) if file_name != '': _pixbuf.save(file_name, 'png') return _pixbuf def save_svg(string, file_name): """ Write a string to a file. """ file_handle = file(file_name, "w") file_handle.write(string) file_handle.close() def get_pixbuf_from_journal(dsobject, w, h): """ Load a pixbuf from a Journal object. """ try: _pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(dsobject.file_path, int(w), int(h)) except: try: _pixbufloader = \ gtk.gdk.pixbuf_loader_new_with_mime_type('image/png') _pixbufloader.set_size(min(300, int(w)), min(225, int(h))) _pixbufloader.write(dsobject.metadata['preview']) _pixbufloader.close() _pixbuf = _pixbufloader.get_pixbuf() except: _pixbuf = None return _pixbuf def get_path(activity, subpath): """ Find a Rainbow-approved place for temporary files. """ try: return(os.path.join(activity.get_activity_root(), subpath)) except: # Early versions of Sugar didn't support get_activity_root() return(os.path.join(os.environ['HOME'], ".sugar/default", "org.laptop.TurtleArtActivity", subpath)) def image_to_base64(pixbuf, path_name): """ Convert an image to base64-encoded data """ file_name = os.path.join(path_name, 'imagetmp.png') if pixbuf != None: pixbuf.save(file_name, "png") base64 = os.path.join(path_name, 'base64tmp') cmd = "base64 <" + file_name + " >" + base64 subprocess.check_call(cmd, shell=True) file_handle = open(base64, 'r') data = file_handle.read() file_handle.close() return data def base64_to_image(data, path_name): """ Convert base64-encoded data to an image """ base64 = os.path.join(path_name, 'base64tmp') file_handle = open(base64, 'w') file_handle.write(data) file_handle.close() file_name = os.path.join(path_name, 'imagetmp.png') cmd = "base64 -d <" + base64 + ">" + file_name subprocess.check_call(cmd, shell=True) return file_name def movie_media_type(name): """ Is it movie media? """ return name.lower().endswith(('.ogv', '.vob', '.mp4', '.wmv', '.mov', '.mpeg', 'ogg')) def audio_media_type(name): """ Is it audio media? """ return name.lower().endswith(('.oga', '.m4a')) def image_media_type(name): """ Is it image media? """ return name.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.tiff', '.tif', '.svg')) def text_media_type(name): """ Is it text media? """ return name.lower().endswith(('.txt', '.py', '.lg', '.rtf')) def round_int(num): """ Remove trailing decimal places if number is an int """ try: float(num) except TypeError: raise pythonerror("#syntaxerror") if int(float(num)) == num: return int(num) else: if float(num) < 0: _nn = int((float(num) - 0.005) * 100) / 100. else: _nn = int((float(num) + 0.005) * 100) / 100. if int(float(_nn)) == _nn: return int(_nn) return _nn def calc_image_size(spr): """ Calculate the maximum size for placing an image onto a sprite. """ return int(max(spr.label_safe_width(), 1)), \ int(max(spr.label_safe_height(), 1)) # Collapsible stacks live between 'sandwichtop' and 'sandwichbottom' blocks def reset_stack_arm(top): """ When we undock, retract the 'arm' that extends from 'sandwichtop'. """ if top is not None and top.name in ['sandwichtop', 'sandwichtop_no_label']: if top.ey > 0: top.reset_y() def grow_stack_arm(top): """ When we dock, grow an 'arm' from 'sandwichtop'. """ if top is not None and top.name in ['sandwichtop', 'sandwichtop_no_label']: _bot = find_sandwich_bottom(top) if _bot is None: return if top.ey > 0: top.reset_y() _ty = top.spr.get_xy()[1] _th = top.spr.get_dimensions()[1] _by = _bot.spr.get_xy()[1] _dy = _by - (_ty + _th) if _dy > 0: top.expand_in_y(_dy / top.scale) top.refresh() def find_sandwich_top(blk): """ Find the sandwich top above this block. """ # Always follow the main branch of a flow: the first connection. _blk = blk.connections[0] while _blk is not None: if _blk.name in COLLAPSIBLE: return None if _blk.name in ['repeat', 'if', 'ifelse', 'forever', 'while']: if blk != _blk.connections[len(_blk.connections) - 1]: return None if _blk.name in ['sandwichtop', 'sandwichtop_no_label', 'sandwichtop_no_arm', 'sandwichtop_no_arm_no_label']: return _blk blk = _blk _blk = _blk.connections[0] return None def find_sandwich_bottom(blk): """ Find the sandwich bottom below this block. """ # Always follow the main branch of a flow: the last connection. _blk = blk.connections[len(blk.connections) - 1] while _blk is not None: if _blk.name in ['sandwichtop', 'sandwichtop_no_label', 'sandwichtop_no_arm', 'sandwichtop_no_arm_no_label']: return None if _blk.name in COLLAPSIBLE: return _blk _blk = _blk.connections[len(_blk.connections) - 1] return None def find_sandwich_top_below(blk): """ Find the sandwich top below this block. """ if blk.name in ['sandwichtop', 'sandwichtop_no_label', 'sandwichtop_no_arm', 'sandwichtop_no_arm_no_label']: return blk # Always follow the main branch of a flow: the last connection. _blk = blk.connections[len(blk.connections) - 1] while _blk is not None: if _blk.name in ['sandwichtop', 'sandwichtop_no_label', 'sandwichtop_no_arm', 'sandwichtop_no_arm_no_label']: return _blk _blk = _blk.connections[len(_blk.connections) - 1] return None def restore_stack(top): """ Restore the blocks between the sandwich top and sandwich bottom. """ _group = find_group(top.connections[len(top.connections) - 1]) _hit_bottom = False _bot = find_sandwich_bottom(top) for _blk in _group: if not _hit_bottom and _blk == _bot: _hit_bottom = True if len(_blk.values) == 0: _blk.values.append(0) else: _blk.values[0] = 0 _olddx = _blk.docks[1][2] _olddy = _blk.docks[1][3] # Replace 'sandwichcollapsed' shape with 'sandwichbottom' shape _blk.name = 'sandwichbottom' _blk.spr.set_label(' ', 1) _blk.svg.set_show(False) _blk.svg.set_hide(True) _blk.refresh() # Redock to previous block in group _you = _blk.connections[0] (_yx, _yy) = _you.spr.get_xy() _yd = _you.docks[len(_you.docks) - 1] (_bx, _by) = _blk.spr.get_xy() _dx = _yx + _yd[2] - _blk.docks[0][2] - _bx _dy = _yy + _yd[3] - _blk.docks[0][3] - _by _blk.spr.move_relative((_dx, _dy)) # Since the shapes have changed, the dock positions have too. _newdx = _blk.docks[1][2] _newdy = _blk.docks[1][3] _dx += _newdx - _olddx _dy += _newdy - _olddy else: if not _hit_bottom: _blk.spr.set_layer(BLOCK_LAYER) _blk.status = None else: _blk.spr.move_relative((_dx, _dy)) # Add 'sandwichtop' arm if top.name == 'sandwichtop_no_arm': top.name = 'sandwichtop' else: top.name = 'sandwichtop_no_label' top.spr.set_label(' ', 1) top.resize() top.refresh() grow_stack_arm(top) def uncollapse_forks(top, looping=False): """ From the top, find and restore any collapsible stacks on forks. """ if top == None: return if looping and top.name in ['sandwichtop_no_arm', 'sandwichtop_no_arm_no_label']: restore_stack(top) return if len(top.connections) == 0: return _blk = top.connections[len(top.connections) - 1] while _blk is not None: if _blk.name in COLLAPSIBLE: return if _blk.name in ['sandwichtop_no_arm', 'sandwichtop_no_arm_no_label']: restore_stack(_blk) return # Follow a fork if _blk.name in ['repeat', 'if', 'ifelse', 'forever', 'while', 'until']: top = find_sandwich_top_below( _blk.connections[len(_blk.connections) - 2]) if top is not None: uncollapse_forks(top, True) if _blk.name == 'ifelse': top = find_sandwich_top_below( _blk.connections[len(_blk.connections) - 3]) if top is not None: uncollapse_forks(top, True) _blk = _blk.connections[len(_blk.connections) - 1] return def collapse_stack(top): """ Hide all the blocks between the sandwich top and sandwich bottom. """ # First uncollapse any nested stacks if top == None or top.spr == None: return uncollapse_forks(top) _hit_bottom = False _bot = find_sandwich_bottom(top) _group = find_group(top.connections[len(top.connections) - 1]) for _blk in _group: if not _hit_bottom and _blk == _bot: _hit_bottom = True # Replace 'sandwichbottom' with invisible 'sandwichcollapsed' if len(_blk.values) == 0: _blk.values.append(1) else: _blk.values[0] = 1 _olddx = _blk.docks[1][2] _olddy = _blk.docks[1][3] _blk.name = 'sandwichcollapsed' _blk.resize() _you = find_sandwich_top(_blk) (_yx, _yy) = _you.spr.get_xy() _yd = _you.docks[len(_you.docks) - 1] (_bx, _by) = _blk.spr.get_xy() _dx = _yx + _yd[2] - _blk.docks[0][2] - _bx _dy = _yy + _yd[3] - _blk.docks[0][3] - _by _blk.spr.move_relative((_dx, _dy)) # Since the shapes have changed, the dock positions have too. _newdx = _blk.docks[1][2] _newdy = _blk.docks[1][3] _dx += _newdx - _olddx _dy += _newdy - _olddy else: if not _hit_bottom: _blk.spr.set_layer(HIDE_LAYER) _blk.status = 'collapsed' else: _blk.spr.move_relative((_dx, _dy)) # Remove 'sandwichtop' arm if top.name == 'sandwichtop' or top.name == 'sandwichtop_no_arm': top.name = 'sandwichtop_no_arm' else: top.name = 'sandwichtop_no_arm_no_label' top.spr.set_label(' ') top.spr.set_label(' ', 1) top.resize() top.spr.set_label(_('click to open'), 1) top.resize() top.refresh() def collapsed(blk): """ Is this stack collapsed? """ if blk is not None and blk.name in COLLAPSIBLE and\ len(blk.values) == 1 and blk.values[0] != 0: return True return False def collapsible(blk): """ Can this stack be collapsed? """ if blk is None or blk.name not in COLLAPSIBLE: return False if find_sandwich_top(blk) is None: return False return True def hide_button_hit(spr, x, y): """ Did the sprite's hide (contract) button get hit? """ _red, _green, _blue, _alpha = spr.get_pixel((x, y)) if _red == HIT_HIDE: return True else: return False def show_button_hit(spr, x, y): """ Did the sprite's show (expand) button get hit? """ _red, _green, _blue, _alpha = spr.get_pixel((x, y)) if _green == HIT_SHOW: return True else: return False def numeric_arg(value): """ Dock test: looking for a numeric value """ if type(convert(value, float)) is float: return True return False def zero_arg(value): """ Dock test: looking for a zero argument """ if numeric_arg(value): if convert(value, float) == 0: return True return False def neg_arg(value): """ Dock test: looking for a negative argument """ if numeric_arg(value): if convert(value, float) < 0: return True return False def journal_check(blk1, blk2, dock1, dock2): """ Dock blocks only if arg is Journal block """ if blk1 == None or blk2 == None: return True if (blk1.name == 'skin' and dock1 == 1) and blk2.name != 'journal': return False if (blk2.name == 'skin' and dock2 == 1) and blk1.name != 'journal': return False return True def arithmetic_check(blk1, blk2, dock1, dock2): """ Dock strings only if they convert to numbers. Avoid /0 and root(-1)""" if blk1 == None or blk2 == None: return True if blk1.name in ['sqrt', 'number', 'string'] and\ blk2.name in ['sqrt', 'number', 'string']: if blk1.name == 'number' or blk1.name == 'string': if not numeric_arg(blk1.values[0]) or neg_arg(blk1.values[0]): return False elif blk2.name == 'number' or blk2.name == 'string': if not numeric_arg(blk2.values[0]) or neg_arg(blk2.values[0]): return False elif blk1.name in ['division2', 'number', 'string'] and\ blk2.name in ['division2', 'number', 'string']: if blk1.name == 'number' or blk1.name == 'string': if not numeric_arg(blk1.values[0]): return False if dock2 == 2 and zero_arg(blk1.values[0]): return False elif blk2.name == 'number' or blk2.name == 'string': if not numeric_arg(blk2.values[0]): return False if dock1 == 2 and zero_arg(blk2.values[0]): return False elif blk1.name in ['product2', 'minus2', 'random', 'remainder2', 'string'] and\ blk2.name in ['product2', 'minus2', 'random', 'remainder2', 'string']: if blk1.name == 'string': if not numeric_arg(blk1.values[0]): return False elif blk1.name == 'string': if not numeric_arg(blk2.values[0]): return False elif blk1.name in ['greater2', 'less2'] and blk2.name == 'string': # Non-numeric stings are OK if only both args are strings; # Lots of test conditions... if dock1 == 1 and blk1.connections[2] is not None: if blk1.connections[2].name == 'number': if not numeric_arg(blk2.values[0]): return False elif dock1 == 2 and blk1.connections[1] is not None: if blk1.connections[1].name == 'number': if not numeric_arg(blk2.values[0]): return False elif blk2.name in ['greater2', 'less2'] and blk1.name == 'string': if dock2 == 1 and blk2.connections[2] is not None: if blk2.connections[2].name == 'number': if not numeric_arg(blk1.values[0]): return False elif dock2 == 2 and blk2.connections[1] is not None: if blk2.connections[1].name == 'number': if not numeric_arg(blk1.values[0]): return False elif blk1.name in ['greater2', 'less2'] and blk2.name == 'number': if dock1 == 1 and blk1.connections[2] is not None: if blk1.connections[2].name == 'string': if not numeric_arg(blk1.connections[2].values[0]): return False elif dock1 == 2 and blk1.connections[1] is not None: if blk1.connections[1].name == 'string': if not numeric_arg(blk1.connections[1].values[0]): return False elif blk2.name in ['greater2', 'less2'] and blk1.name == 'number': if dock2 == 1 and blk2.connections[2] is not None: if blk2.connections[2].name == 'string': if not numeric_arg(blk2.connections[2].values[0]): return False elif dock2 == 2 and blk2.connections[1] is not None: if blk2.connections[1].name == 'string': if not numeric_arg(blk2.connections[1].values[0]): return False return True def xy(event): """ Where is the mouse event? """ return map(int, event.get_coords()) """ Utilities related to finding blocks in stacks. """ def find_block_to_run(blk): """ Find a stack to run (any stack without a 'def action'on the top). """ _top = find_top_block(blk) if blk == _top and blk.name[0:3] is not 'def': return True else: return False def find_top_block(blk): """ Find the top block in a stack. """ if blk is None: return None if len(blk.connections) == 0: return blk while blk.connections[0] is not None: blk = blk.connections[0] return blk def find_start_stack(blk): """ Find a stack with a 'start' block on top. """ if blk is None: return False if find_top_block(blk).name == 'start': return True else: return False def find_group(blk): """ Find the connected group of block in a stack. """ if blk is None: return [] _group = [blk] if blk.connections is not None: for _blk2 in blk.connections[1:]: if _blk2 is not None: _group.extend(find_group(_blk2)) return _group def find_blk_below(blk, name): """ Find a specific block below this block. """ if blk == None or len(blk.connections) == 0: return _group = find_group(blk) for _gblk in _group: if _gblk.name == name: return _gblk return None def get_hardware(): """ Determine whether we are using XO 1.0, 1.5, or "unknown" hardware """ bus = dbus.SystemBus() comp_obj = None try: comp_obj = bus.get_object('org.freedesktop.Hal', '/org/freedesktop/Hal/devices/computer') except dbus.exceptions.DBusException: error_output('Unable to get dbus object \ /org/freedesktop/Hal/devices/computer') if comp_obj is not None: dev = dbus.Interface(comp_obj, 'org.freedesktop.Hal.Device') if dev.PropertyExists('system.hardware.vendor') and \ dev.PropertyExists('system.hardware.version'): if dev.GetProperty('system.hardware.vendor') == 'OLPC': if dev.GetProperty('system.hardware.version') == '1.5': return XO15 else: return XO1 else: return UNKNOWN if os.path.exists('/etc/olpc-release') or \ os.path.exists('/sys/power/olpc-pm'): return XO1 else: return UNKNOWN