From 81ba8fd9fa0e2fbbee1ef3534a96104de4cd7079 Mon Sep 17 00:00:00 2001 From: Walter Bender Date: Wed, 17 Mar 2010 18:39:20 +0000 Subject: Merge branch 'master' of git://git.sugarlabs.org/turtleart/refactoring Conflicts: NEWS activity/activity.info tagplay.py tawindow.py --- (limited to 'tautils.py') diff --git a/tautils.py b/tautils.py new file mode 100644 index 0000000..77eb3f5 --- /dev/null +++ b/tautils.py @@ -0,0 +1,693 @@ +#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 +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 STRING_OR_NUMBER_ARGS, HIDE_LAYER, CONTENT_ARGS, \ + COLLAPSIBLE, BLOCK_LAYER, CONTENT_BLOCKS +from StringIO import StringIO +import os.path +from gettext import gettext as _ + +class logoerror(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +''' +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 trreated as ints, then +floats. +''' +def convert(x, fn, try_ord=True): + 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 + text = text.lstrip() + text = text.replace('\12','') + text = text.replace('\00','') + _io = StringIO(text.rstrip()) + _listdata = jload(_io) + # 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 None: + return None + return connection.id + +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) + +# +# We try to maintain read-compatibility with all versions of Turtle Art. +# Try pickle first; then different versions of json. +# +def data_from_file(ta_file): + """ Open the .ta file, ignoring any .png file that might be present. """ + file_handle = open(ta_file, "r") + 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) + +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) + +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, activity): + """ Convert an image to base64 """ + _file_name = os.path.join(get_path(activity, 'instance'), 'imagetmp.png') + if pixbuf != None: + pixbuf.save(_file_name, "png") + _base64 = os.path.join(get_path(activity, 'instance'), '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 movie_media_type(name): + """ Is it movie media? """ + return name.endswith(('.ogv', '.vob', '.mp4', '.wmv', '.mov', '.mpeg')) + +def audio_media_type(name): + """ Is it audio media? """ + return name.endswith(('.ogg', '.oga', '.m4a')) + +def image_media_type(name): + """ Is it image media? """ + return name.endswith(('.png', '.jpg', '.jpeg', '.gif', '.tiff', '.tif', + '.svg')) +def text_media_type(name): + """ Is it text media? """ + return name.endswith(('.txt', '.py', '.lg', '.doc', '.rtf')) + +def round_int(num): + """ Remove trailing decimal places if number is an int """ + try: + float(num) + except TypeError: + raise logoerror("#syntaxerror") + + if int(float(num)) == num: + return int(num) + else: + _nn = int(float(num+0.05)*10)/10. + 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 spr.label_safe_width(), spr.label_safe_height() + + + +# 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 == 'sandwichtop': + 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 == 'sandwichtop': + _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 == 'sandwichtop' or _blk.name == 'sandwichtop2': + 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 == 'sandwichtop' or _blk.name == 'sandwichtop2': + 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 == 'sandwichtop' or blk.name == 'sandwichtop2': + 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 == 'sandwichtop' or _blk.name == 'sandwichtop2': + 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(' ') + _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 + top.name = 'sandwichtop' + 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 == 'sandwichtop' or top.name == 'sandwichtop2': + 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 == 'sandwichtop' or _blk.name == 'sandwichtop2': + 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 + 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' shape with 'sandwichcollapsed' shape + 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.svg.set_show(True) + _blk.svg.set_hide(False) + _blk._dx = 0 + _blk._ey = 0 + _blk.spr.set_label(' ') + _blk.resize() + _blk.spr.set_label(_('click to open')) + _blk.resize() + # Redock to sandwich top in group + _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 + top.name = 'sandwichtop2' + 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 == 255 and _green == 0) or _green == 255: + 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 == 254: + 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 dock_dx_dy(block1, dock1n, block2, dock2n): + """ Find the distance between the dock points of two blocks. """ + _dock1 = block1.docks[dock1n] + _dock2 = block2.docks[dock2n] + _d1type, _d1dir, _d1x, _d1y = _dock1[0:4] + _d2type, _d2dir, _d2x, _d2y = _dock2[0:4] + if block1 == block2: + return (100, 100) + if _d1dir == _d2dir: + return (100, 100) + if (_d2type is not 'number') or (dock2n is not 0): + if block1.connections is not None and \ + dock1n < len(block1.connections) and \ + block1.connections[dock1n] is not None: + return (100, 100) + if block2.connections is not None and \ + dock2n < len(block2.connections) and \ + block2.connections[dock2n] is not None: + return (100, 100) + if _d1type != _d2type: + if block1.name in STRING_OR_NUMBER_ARGS: + if _d2type == 'number' or _d2type == 'string': + pass + elif block1.name in CONTENT_ARGS: + if _d2type in CONTENT_BLOCKS: + pass + else: + return (100, 100) + (_b1x, _b1y) = block1.spr.get_xy() + (_b2x, _b2y) = block2.spr.get_xy() + return ((_b1x + _d1x) - (_b2x + _d2x), (_b1y + _d1y) - (_b2y + _d2y)) + +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 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 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 olpc_xo_1(): + """ Is the an OLPC XO-1 or XO-1.5? """ + return os.path.exists('/etc/olpc-release') or \ + os.path.exists('/sys/power/olpc-pm') + +def walk_stack(tw, blk): + """ Convert blocks to logo psuedocode. """ + top = find_top_block(blk) + if blk == top: + code = tw.lc.run_blocks(top, tw.block_list.list, False) + return code + else: + return [] -- cgit v0.9.1