From 460727e4e87f06e44d84c56b386a3e54fc1eff16 Mon Sep 17 00:00:00 2001 From: Walter Bender Date: Wed, 18 Dec 2013 19:21:39 +0000 Subject: sync to 195 --- diff --git a/.gitignore b/.gitignore index f3d74a9..37c737b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.pyc *~ +locale +dist diff --git a/TurtleArt/sprites.py b/TurtleArt/sprites.py index 7483c12..3f5d7df 100644 --- a/TurtleArt/sprites.py +++ b/TurtleArt/sprites.py @@ -106,7 +106,7 @@ class Sprites: def length_of_list(self): ''' How many sprites are there? ''' - return(len(self.list)) + return len(self.list) def append_to_list(self, spr): ''' Append a new sprite to the end of the list. ''' @@ -122,9 +122,7 @@ class Sprites: self.list.insert(i, spr) def find_in_list(self, spr): - if spr in self.list: - return True - return False + return (spr in self.list) def remove_from_list(self, spr): ''' Remove a sprite from the list. ''' @@ -258,7 +256,8 @@ class Sprite: if layer is not None: self.layer = layer for i in range(self._sprites.length_of_list()): - if self.layer < self._sprites.get_sprite(i).layer: + spr = self._sprites.get_sprite(i) + if spr is not None and self.layer < spr.layer: self._sprites.insert_in_list(self, i) self.inval() return diff --git a/TurtleArt/tabasics.py b/TurtleArt/tabasics.py index 3d1eb10..a7dbd5b 100644 --- a/TurtleArt/tabasics.py +++ b/TurtleArt/tabasics.py @@ -20,72 +20,94 @@ #THE SOFTWARE. ''' -This file contains the constants that by-in-large determine the -behavior of Turtle Art. Notably, the block palettes are defined -below. If you want to add a new block to Turtle Art, you could -simply add a block of code to this file or to turtle_block_plugin.py, -which contains additional blocks. (Even better, write your own plugin!!) +This file contains the constants that by-in-large determine the +behavior of Turtle Art. Notably, the block palettes are defined +below. If you want to add a new block to Turtle Art, you could +simply add a block of code to this file or to +../plugins/turtle_blocks_extras/turtle_blocks_extras.py , +which contains additional blocks. (Even better, write your own +plugin!!) Adding a new palette is simply a matter of: + palette = make_palette('mypalette', # the name of your palette colors=["#00FF00", "#00A000"], help_string=_('Palette of my custom commands')) -For example, if we want to add a new turtle command, 'uturn', we'd use the -add_block method in the Palette class. +For example, if we want to add a new turtle command, 'uturn', +we'd use the `add_block` method in the Palette class. + palette.add_block('uturn', # the name of your block style='basic-style', # the block style label=_('u turn'), # the label for the block prim_name='uturn', # code reference (see below) help_string=_('turns the turtle 180 degrees')) - # Next, you need to define what your block will do: - # def_prim takes 3 arguments: the primitive name, the number of - # arguments -- 0 in this case -- and the function to call -- in this - # case, we define the _prim_uturn function to set heading += 180. - self.tw.lc.def_prim('uturn', 0, lambda self: self._prim_uturn) - def _prim_uturn(self): - value = self.tw.turtles.get_active_turtle().get_heading() + 180 - self.tw.turtles.get_active_turtle().set_heading(value) +Next, you need to define what your block will do: def_prim takes +3 arguments: the primitive name, the number of arguments --- 0 +in this case --- and a Primitive object. A Primitive object +represents the statement to be executed when the block is +executed in Turtle Art. For the 'uturn' block, we would like the +statement to look roughly like this: + + Turtle.set_heading(plus(Turtle.get_heading(), 180)) + +Formally, a Primitive object consists of a function, its return +type, and descriptions of its arguments and keyword arguments. +The return type is not a Python type, but a type from Turtle +Art's internal type system. All available types are defined as +constants in tatype.py . + +In this case, we know in advance which arguments each function +gets, so we can use ConstantArg objects as argument descrip- +tions. (For examples where the arguments come from other blocks, +please refer to ../doc/primitives-with-arguments.md .) Note that +Primitive objects can be arguments to other Primitive objects. +This leads to the following tree-like structure for our 'uturn' +block: + + prim_uturn = Primitive(Turtle.set_heading, + arg_descs=[ConstantArg(Primitive( + Primitive.plus, return_type=TYPE_NUMBER, + arg_descs=[ConstantArg(Primitive( + Turtle.get_heading, return_type=TYPE_NUMBER)), + ConstantArg(180)]))], + call_afterwards=self.after_uturn) + + self.tw.lc.def_prim('uturn', 0, prim_uturn) + + # somewhere else in the same class: + def after_uturn(self, value): if self.tw.lc.update_values: self.tw.lc.update_label_value('heading', value) -That's it. When you next run Turtle Art, you will have a 'uturn' block -on the 'mypalette' palette. +The `call_afterwards` attribute is a simple function that is +called just after executing the block. It is often used for +updating GUI labels. -You will have to create icons for the palette-selector buttons. These -are kept in the icons subdirectory. You need two icons: -mypaletteoff.svg and mypaletteon.svg, where 'mypalette' is the same -string as the entry you used in instantiating the Palette class. Note -that the icons should be the same size (55x55) as the others. (This is -the default icon size for Sugar toolbars.) -''' +That's it. When you next run Turtle Art, you will have a 'uturn' +block on the 'mypalette' palette. -from time import time, sleep -from math import sqrt -from random import uniform +You will have to create icons for the palette-selector buttons. +These are kept in the 'icons' subdirectory. You need two icons: +mypaletteoff.svg and mypaletteon.svg, where 'mypalette' is the +same string as the entry you used in instantiating the Palette +object. Note that the icons should be the same size (55x55) as +the others. (This is the default icon size for Sugar toolbars.) +''' +from time import time from gettext import gettext as _ from tapalette import (make_palette, define_logo_function) -from talogo import (primitive_dictionary, logoerror) -from tautils import (convert, chr_to_ord, round_int, strtype, debug_output) -from taconstants import (COLORDICT, CONSTANTS) - - -def _color_to_num(c): - if COLORDICT[c][0] is None: - return(COLORDICT[c][1]) - else: - return(COLORDICT[c][0]) - - -def _num_type(x): - ''' Is x a number type? ''' - if isinstance(x, (int, float)): - return True - return False +from talogo import primitive_dictionary +from taconstants import (Color, CONSTANTS) +from taprimitive import (ArgSlot, ConstantArg, or_, Primitive) +from tatype import (TYPE_BOOL, TYPE_BOX, TYPE_CHAR, TYPE_COLOR, TYPE_FLOAT, + TYPE_INT, TYPE_NUMBER, TYPE_NUMERIC_STRING, TYPE_OBJECT, + TYPE_STRING) +from taturtle import Turtle def _millisecond(): @@ -99,6 +121,15 @@ class Palettes(): def __init__(self, turtle_window): self.tw = turtle_window + self.prim_cache = { + "minus": Primitive(Primitive.minus, + return_type=TYPE_NUMBER, + arg_descs=[ArgSlot(TYPE_NUMBER)]), + "ord": Primitive(ord, + return_type=TYPE_INT, + arg_descs=[ArgSlot(TYPE_CHAR)]) + } # avoid several Primitives of the same function + self._turtle_palette() self._pen_palette() @@ -119,13 +150,11 @@ class Palettes(): def _turtle_palette(self): ''' The basic Turtle Art turtle palette ''' - debug_output('creating %s palette' % _('turtle'), - self.tw.running_sugar) palette = make_palette('turtle', colors=["#00FF00", "#00A000"], - help_string=_('Palette of turtle commands')) + help_string=_('Palette of turtle commands'), + translation=_('turtle')) - primitive_dictionary['move'] = self._prim_move palette.add_block('forward', style='basic-style-1arg', label=_('forward'), @@ -134,10 +163,10 @@ class Palettes(): logo_command='forward', help_string=_('moves turtle forward')) self.tw.lc.def_prim( - 'forward', - 1, - lambda self, x: primitive_dictionary['move']( - self.tw.turtles.get_active_turtle().forward, x)) + 'forward', 1, + Primitive(Turtle.forward, + arg_descs=[ArgSlot(TYPE_NUMBER)], + call_afterwards=self.after_move)) palette.add_block('back', style='basic-style-1arg', @@ -146,13 +175,12 @@ class Palettes(): default=100, logo_command='back', help_string=_('moves turtle backward')) - self.tw.lc.def_prim('back', 1, - lambda self, x: - primitive_dictionary['move'] - (self.tw.turtles.get_active_turtle().forward, x, - reverse=True)) + self.tw.lc.def_prim( + 'back', 1, + Primitive(Turtle.backward, + arg_descs=[ArgSlot(TYPE_NUMBER)], + call_afterwards=self.after_move)) - primitive_dictionary['clean'] = self._prim_clear palette.add_block('clean', style='basic-style-extended-vertical', label=_('clean'), @@ -160,12 +188,20 @@ class Palettes(): logo_command='clean', help_string=_('clears the screen and reset the \ turtle')) - self.tw.lc.def_prim( - 'clean', - 0, - lambda self: primitive_dictionary['clean']()) + self.tw.lc.def_prim('clean', 0, + Primitive(Primitive.group, arg_descs=[ + ConstantArg([ + Primitive(self.tw.clear_plugins), + Primitive(self.tw.lc.stop_playing_media), + Primitive(self.tw.lc.reset_scale), + Primitive(self.tw.lc.reset_timer), + Primitive(self.tw.lc.clear_value_blocks), + Primitive(self.tw.canvas.clearscreen), + Primitive(self.tw.lc.reset_internals), + Primitive(self.tw.turtles.reset_turtles), + Primitive(self.tw.lc.active_turtle) + ])])) - primitive_dictionary['right'] = self._prim_right palette.add_block('left', style='basic-style-1arg', label=_('left'), @@ -175,8 +211,10 @@ turtle')) help_string=_('turns turtle counterclockwise (angle \ in degrees)')) self.tw.lc.def_prim( - 'left', 1, lambda self, - x: primitive_dictionary['right'](x, reverse=True)) + 'left', 1, + Primitive(Turtle.left, + arg_descs=[ArgSlot(TYPE_NUMBER)], + call_afterwards=self.after_right)) palette.add_block('right', style='basic-style-1arg', @@ -187,11 +225,11 @@ in degrees)')) help_string=_('turns turtle clockwise (angle in \ degrees)')) self.tw.lc.def_prim( - 'right', - 1, - lambda self, x: primitive_dictionary['right'](x)) + 'right', 1, + Primitive(Turtle.right, + arg_descs=[ArgSlot(TYPE_NUMBER)], + call_afterwards=self.after_right)) - primitive_dictionary['arc'] = self._prim_arc palette.add_block('arc', style='basic-style-2arg', label=[_('arc'), _('angle'), _('radius')], @@ -200,10 +238,11 @@ degrees)')) logo_command='taarc', help_string=_('moves turtle along an arc')) self.tw.lc.def_prim( - 'arc', - 2, - lambda self, x, y: primitive_dictionary['arc']( - self.tw.turtles.get_active_turtle().arc, x, y)) + 'arc', 2, + Primitive(Turtle.arc, + arg_descs=[ArgSlot(TYPE_NUMBER), + ArgSlot(TYPE_NUMBER)], + call_afterwards=self.after_arc)) define_logo_function('taarc', 'to taarc :a :r\nrepeat round :a \ [right 1 forward (0.0175 * :r)]\nend\n') @@ -216,13 +255,12 @@ degrees)')) help_string=_('moves turtle to position xcor, ycor; \ (0, 0) is in the center of the screen.')) self.tw.lc.def_prim( - 'setxy2', - 2, - lambda self, x, y: primitive_dictionary['move']( - self.tw.turtles.get_active_turtle().set_xy, x, y)) + 'setxy2', 2, + Primitive(Turtle.set_xy, + arg_descs=[ArgSlot(TYPE_NUMBER), ArgSlot(TYPE_NUMBER)], + call_afterwards=self.after_move)) define_logo_function('tasetxy', 'to tasetxy :x :y\nsetxy :x :y\nend\n') - primitive_dictionary['set'] = self._prim_set palette.add_block('seth', style='basic-style-1arg', label=_('set heading'), @@ -232,10 +270,11 @@ degrees)')) help_string=_('sets the heading of the turtle (0 is \ towards the top of the screen.)')) self.tw.lc.def_prim( - 'seth', - 1, - lambda self, x: primitive_dictionary['set']( - 'heading', self.tw.turtles.get_active_turtle().set_heading, x)) + 'seth', 1, + Primitive(Turtle.set_heading, + arg_descs=[ArgSlot(TYPE_NUMBER)], + call_afterwards=lambda value: self.after_set( + 'heading', value))) palette.add_block('xcor', style='box-style', @@ -246,10 +285,11 @@ the turtle (can be used in place of a number block)'), prim_name='xcor', logo_command='xcor') self.tw.lc.def_prim( - 'xcor', - 0, - lambda self: self.tw.turtles.get_active_turtle().get_xy()[0] / - self.tw.coord_scale) + 'xcor', 0, + Primitive(Primitive.divide, return_type=TYPE_FLOAT, + arg_descs=[ConstantArg(Primitive(Turtle.get_x)), + ConstantArg(Primitive( + self.tw.get_coord_scale))])) palette.add_block('ycor', style='box-style', @@ -260,10 +300,11 @@ the turtle (can be used in place of a number block)'), prim_name='ycor', logo_command='ycor') self.tw.lc.def_prim( - 'ycor', - 0, - lambda self: self.tw.turtles.get_active_turtle().get_xy()[1] / - self.tw.coord_scale) + 'ycor', 0, + Primitive(Primitive.divide, return_type=TYPE_FLOAT, + arg_descs=[ConstantArg(Primitive(Turtle.get_y)), + ConstantArg(Primitive( + self.tw.get_coord_scale))])) palette.add_block('heading', style='box-style', @@ -273,16 +314,14 @@ turtle (can be used in place of a number block)'), value_block=True, prim_name='heading', logo_command='heading') - self.tw.lc.def_prim( - 'heading', - 0, - lambda self: self.tw.turtles.get_active_turtle().get_heading()) + self.tw.lc.def_prim('heading', 0, + Primitive( + Turtle.get_heading, return_type=TYPE_NUMBER)) - # This block is used for holding the remote turtle name palette.add_block('turtle-label', hidden=True, style='blank-style', - label=['remote turtle name']) + label=['turtle']) # Deprecated palette.add_block('setxy', @@ -295,22 +334,21 @@ turtle (can be used in place of a number block)'), help_string=_('moves turtle to position xcor, ycor; \ (0, 0) is in the center of the screen.')) self.tw.lc.def_prim( - 'setxy', - 2, - lambda self, x, y: primitive_dictionary['move']( - self.tw.turtles.get_active_turtle().set_xy, x, y, - pendown=False)) + 'setxy', 2, + Primitive(Turtle.set_xy, + arg_descs=[ArgSlot(TYPE_NUMBER), ArgSlot(TYPE_NUMBER)], + kwarg_descs={'pendown': ConstantArg(False)}, + call_afterwards=self.after_move)) define_logo_function('tasetxypenup', 'to tasetxypenup :x :y\npenup\n\ setxy :x :y\npendown\nend\n') def _pen_palette(self): ''' The basic Turtle Art pen palette ''' - debug_output('creating %s palette' % _('pen'), - self.tw.running_sugar) palette = make_palette('pen', colors=["#00FFFF", "#00A0A0"], - help_string=_('Palette of pen commands')) + help_string=_('Palette of pen commands'), + translation=_('pen')) palette.add_block('fillscreen', hidden=True, @@ -322,9 +360,9 @@ setxy :x :y\npendown\nend\n') help_string=_('fills the background with (color, \ shade)')) self.tw.lc.def_prim( - 'fillscreen', - 2, - lambda self, x, y: self.tw.canvas.fillscreen(x, y)) + 'fillscreen', 2, + Primitive(self.tw.canvas.fillscreen, + arg_descs=[ArgSlot(TYPE_NUMBER), ArgSlot(TYPE_NUMBER)])) palette.add_block('fillscreen2', style='basic-style-3arg', @@ -336,9 +374,10 @@ shade)')) help_string=_('fills the background with (color, \ shade)')) self.tw.lc.def_prim( - 'fillscreen2', - 3, - lambda self, x, y, z: self.tw.canvas.fillscreen_with_gray(x, y, z)) + 'fillscreen2', 3, + Primitive(self.tw.canvas.fillscreen_with_gray, + arg_descs=[ArgSlot(TYPE_NUMBER), ArgSlot(TYPE_NUMBER), + ArgSlot(TYPE_NUMBER)])) define_logo_function('tasetbackground', 'to tasetbackground :color \ :shade\ntasetshade :shade\nsetbackground :color\nend\n') @@ -352,10 +391,12 @@ shade)')) help_string=_('sets color of the line drawn by the \ turtle')) self.tw.lc.def_prim( - 'setcolor', - 1, - lambda self, x: primitive_dictionary['set']( - 'color', self.tw.turtles.get_active_turtle().set_color, x)) + 'setcolor', 1, + Primitive(Turtle.set_color, + arg_descs=[or_(ArgSlot(TYPE_COLOR), + ArgSlot(TYPE_NUMBER))], + call_afterwards=lambda value: self.after_set( + 'color', value))) palette.add_block('setshade', style='basic-style-1arg', @@ -366,10 +407,11 @@ turtle')) help_string=_('sets shade of the line drawn by the \ turtle')) self.tw.lc.def_prim( - 'setshade', - 1, - lambda self, x: primitive_dictionary['set']( - 'shade', self.tw.turtles.get_active_turtle().set_shade, x)) + 'setshade', 1, + Primitive(Turtle.set_shade, + arg_descs=[ArgSlot(TYPE_NUMBER)], + call_afterwards=lambda value: self.after_set( + 'shade', value))) palette.add_block('setgray', style='basic-style-1arg', @@ -379,10 +421,11 @@ turtle')) help_string=_('sets gray level of the line drawn by \ the turtle')) self.tw.lc.def_prim( - 'setgray', - 1, - lambda self, x: primitive_dictionary['set']( - 'gray', self.tw.turtles.get_active_turtle().set_gray, x)) + 'setgray', 1, + Primitive(Turtle.set_gray, + arg_descs=[ArgSlot(TYPE_NUMBER)], + call_afterwards=lambda value: self.after_set( + 'gray', value))) palette.add_block('color', style='box-style', @@ -392,10 +435,9 @@ in place of a number block)'), value_block=True, prim_name='color', logo_command='pencolor') - self.tw.lc.def_prim( - 'color', - 0, - lambda self: self.tw.turtles.get_active_turtle().get_color()) + self.tw.lc.def_prim('color', 0, + Primitive( + Turtle.get_color, return_type=TYPE_NUMBER)) palette.add_block('shade', style='box-style', @@ -404,10 +446,8 @@ in place of a number block)'), value_block=True, prim_name='shade', logo_command=':shade') - self.tw.lc.def_prim( - 'shade', - 0, - lambda self: self.tw.turtles.get_active_turtle().get_shade()) + self.tw.lc.def_prim('shade', 0, + Primitive(Turtle.get_shade, return_type=TYPE_NUMBER)) palette.add_block('gray', style='box-style', @@ -416,8 +456,9 @@ in place of a number block)'), used in place of a number block)'), value_block=True, prim_name='gray') - self.tw.lc.def_prim('gray', 0, lambda self: - self.tw.turtles.get_active_turtle().get_gray()) + self.tw.lc.def_prim('gray', 0, + Primitive( + Turtle.get_gray, return_type=TYPE_NUMBER)) palette.add_block('penup', style='basic-style-extended-vertical', @@ -426,10 +467,8 @@ used in place of a number block)'), logo_command='penup', help_string=_('Turtle will not draw when moved.')) self.tw.lc.def_prim( - 'penup', - 0, - lambda self: - self.tw.turtles.get_active_turtle().set_pen_state(False)) + 'penup', 0, + Primitive(Turtle.set_pen_state, arg_descs=[ConstantArg(False)])) palette.add_block('pendown', style='basic-style-extended-vertical', @@ -438,10 +477,8 @@ used in place of a number block)'), logo_command='pendown', help_string=_('Turtle will draw when moved.')) self.tw.lc.def_prim( - 'pendown', - 0, - lambda self: - self.tw.turtles.get_active_turtle().set_pen_state(True)) + 'pendown', 0, + Primitive(Turtle.set_pen_state, arg_descs=[ConstantArg(True)])) palette.add_block('penstate', style='boolean-block-style', @@ -449,9 +486,8 @@ used in place of a number block)'), prim_name='penstate', help_string=_('returns True if pen is down')) self.tw.lc.def_prim( - 'penstate', - 0, - lambda self: self.tw.turtles.get_active_turtle().get_pen_state()) + 'penstate', 0, + Primitive(Turtle.get_pen_state, return_type=TYPE_BOOL)) palette.add_block('setpensize', style='basic-style-1arg', @@ -463,8 +499,10 @@ used in place of a number block)'), turtle')) self.tw.lc.def_prim( 'setpensize', 1, - lambda self, x: primitive_dictionary['set'] - ('pensize', self.tw.turtles.get_active_turtle().set_pen_size, x)) + Primitive(Turtle.set_pen_size, + arg_descs=[ArgSlot(TYPE_NUMBER)], + call_afterwards=lambda val: self.after_set( + 'pensize', val))) define_logo_function('tasetpensize', 'to tasetpensize :a\nsetpensize round :a\nend\n') @@ -474,10 +512,7 @@ turtle')) prim_name='startfill', help_string=_('starts filled polygon (used with end \ fill block)')) - self.tw.lc.def_prim( - 'startfill', - 0, - lambda self: self.tw.turtles.get_active_turtle().start_fill()) + self.tw.lc.def_prim('startfill', 0, Primitive(Turtle.start_fill)) palette.add_block('stopfill', style='basic-style-extended-vertical', @@ -485,10 +520,7 @@ fill block)')) prim_name='stopfill', help_string=_('completes filled polygon (used with \ start fill block)')) - self.tw.lc.def_prim( - 'stopfill', - 0, - lambda self: self.tw.turtles.get_active_turtle().stop_fill()) + self.tw.lc.def_prim('stopfill', 0, Primitive(Turtle.stop_fill)) palette.add_block('pensize', style='box-style', @@ -499,33 +531,27 @@ in place of a number block)'), prim_name='pensize', logo_command='pensize') self.tw.lc.def_prim( - 'pensize', - 0, - lambda self: self.tw.turtles.get_active_turtle().get_pen_size()) + 'pensize', 0, + Primitive(Turtle.get_pen_size, return_type=TYPE_NUMBER)) define_logo_function('tapensize', 'to tapensize\noutput first round \ pensize\nend\n') def _color_palette(self): ''' The basic Turtle Art color palette ''' - debug_output('creating %s palette' % _('colors'), - self.tw.running_sugar) palette = make_palette('colors', colors=["#00FFFF", "#00A0A0"], - help_string=_('Palette of pen colors')) - - self._make_constant(palette, 'red', _('red'), CONSTANTS['red']) - self._make_constant(palette, 'orange', _('orange'), - CONSTANTS['orange']) - self._make_constant(palette, 'yellow', _('yellow'), - CONSTANTS['yellow']) - self._make_constant(palette, 'green', _('green'), CONSTANTS['green']) - self._make_constant(palette, 'cyan', _('cyan'), CONSTANTS['cyan']) - self._make_constant(palette, 'blue', _('blue'), CONSTANTS['blue']) - self._make_constant(palette, 'purple', _('purple'), - CONSTANTS['purple']) - self._make_constant(palette, 'white', _('white'), CONSTANTS['white']) - self._make_constant(palette, 'black', _('black'), CONSTANTS['black']) + help_string=_('Palette of pen colors'), + translation=_('colors')) + + color_names = ('red', 'orange', 'yellow', 'green', 'cyan', 'blue', + 'purple', 'white', 'black') + # Need to make sure color names are included in the PO files + color_names_i18n = (_('red'), _('orange'), _('yellow'), _('green'), + _('cyan'), _('blue'), _('purple'), _('white'), + _('black')) + for name in color_names: + self._make_constant(palette, name, _(name), name) # In order to map Turtle Art colors to the standard UCB Logo palette, # we need to define a somewhat complex set of functions. @@ -603,13 +629,11 @@ tasetshade :shade \n') def _numbers_palette(self): ''' The basic Turtle Art numbers palette ''' - debug_output('creating %s palette' % _('numbers'), - self.tw.running_sugar) palette = make_palette('numbers', colors=["#FF00FF", "#A000A0"], - help_string=_('Palette of numeric operators')) + help_string=_('Palette of numeric operators'), + translation=_('numbers')) - primitive_dictionary['plus'] = self._prim_plus palette.add_block('plus2', style='number-style', label='+', @@ -619,9 +643,16 @@ tasetshade :shade \n') logo_command='sum', help_string=_('adds two alphanumeric inputs')) self.tw.lc.def_prim( - 'plus', 2, lambda self, x, y: primitive_dictionary['plus'](x, y)) + 'plus', 2, + # add up two numbers ... + or_(Primitive(Primitive.plus, return_type=TYPE_NUMBER, + arg_descs=[ArgSlot(TYPE_NUMBER), + ArgSlot(TYPE_NUMBER)]), + # ... or concatenate two strings + Primitive(Primitive.plus, return_type=TYPE_STRING, + arg_descs=[ArgSlot(TYPE_STRING), + ArgSlot(TYPE_STRING)]))) - primitive_dictionary['minus'] = self._prim_minus palette.add_block('minus2', style='number-style-porch', label=' –', @@ -631,11 +662,12 @@ tasetshade :shade \n') help_string=_('subtracts bottom numeric input from \ top numeric input')) self.tw.lc.def_prim( - 'minus', 2, lambda self, x, y: primitive_dictionary['minus'](x, y)) + 'minus', 2, + Primitive(Primitive.minus, return_type=TYPE_NUMBER, + arg_descs=[ArgSlot(TYPE_NUMBER), ArgSlot(TYPE_NUMBER)])) define_logo_function('taminus', 'to taminus :y :x\noutput sum :x \ minus :y\nend\n') - primitive_dictionary['product'] = self._prim_product palette.add_block('product2', style='number-style', label='×', @@ -645,9 +677,9 @@ minus :y\nend\n') help_string=_('multiplies two numeric inputs')) self.tw.lc.def_prim( 'product', 2, - lambda self, x, y: primitive_dictionary['product'](x, y)) + Primitive(Primitive.multiply, return_type=TYPE_NUMBER, + arg_descs=[ArgSlot(TYPE_NUMBER), ArgSlot(TYPE_NUMBER)])) - primitive_dictionary['division'] = self._prim_careful_divide palette.add_block('division2', style='number-style-porch', label=' /', @@ -658,9 +690,9 @@ minus :y\nend\n') (numerator) by bottom numeric input (denominator)')) self.tw.lc.def_prim( 'division', 2, - lambda self, x, y: primitive_dictionary['division'](x, y)) + Primitive(Primitive.divide, return_type=TYPE_NUMBER, + arg_descs=[ArgSlot(TYPE_NUMBER), ArgSlot(TYPE_NUMBER)])) - primitive_dictionary['id'] = self._prim_identity palette.add_block('identity2', style='number-style-1arg', label='←', @@ -668,10 +700,26 @@ minus :y\nend\n') prim_name='id', help_string=_('identity operator used for extending \ blocks')) - self.tw.lc.def_prim('id', 1, - lambda self, x: primitive_dictionary['id'](x)) + self.tw.lc.def_prim( + 'id', 1, + # preserve the Type of the argument: try less general types first + or_(Primitive(Primitive.identity, return_type=TYPE_NUMERIC_STRING, + arg_descs=[ArgSlot(TYPE_NUMERIC_STRING)]), + Primitive(Primitive.identity, return_type=TYPE_CHAR, + arg_descs=[ArgSlot(TYPE_CHAR)]), + Primitive(Primitive.identity, return_type=TYPE_COLOR, + arg_descs=[ArgSlot(TYPE_COLOR)]), + Primitive(Primitive.identity, return_type=TYPE_FLOAT, + arg_descs=[ArgSlot(TYPE_FLOAT)]), + Primitive(Primitive.identity, return_type=TYPE_INT, + arg_descs=[ArgSlot(TYPE_INT)]), + Primitive(Primitive.identity, return_type=TYPE_NUMBER, + arg_descs=[ArgSlot(TYPE_NUMBER)]), + Primitive(Primitive.identity, return_type=TYPE_STRING, + arg_descs=[ArgSlot(TYPE_STRING)]), + Primitive(Primitive.identity, return_type=TYPE_OBJECT, + arg_descs=[ArgSlot(TYPE_OBJECT)]))) - primitive_dictionary['remainder'] = self._prim_mod palette.add_block('remainder2', style='number-style-porch', label=_('mod'), @@ -679,11 +727,11 @@ blocks')) prim_name='remainder', logo_command='remainder', help_string=_('modular (remainder) operator')) - self.tw.lc.def_prim('remainder', 2, - lambda self, x, y: - primitive_dictionary['remainder'](x, y)) + self.tw.lc.def_prim( + 'remainder', 2, + Primitive(Primitive.modulo, return_type=TYPE_NUMBER, + arg_descs=[ArgSlot(TYPE_NUMBER), ArgSlot(TYPE_NUMBER)])) - primitive_dictionary['sqrt'] = self._prim_sqrt palette.add_block('sqrt', style='number-style-1arg', label=_('√'), @@ -691,10 +739,11 @@ blocks')) prim_name='sqrt', logo_command='tasqrt', help_string=_('calculates square root')) - self.tw.lc.def_prim('sqrt', 1, - lambda self, x: primitive_dictionary['sqrt'](x)) + self.tw.lc.def_prim( + 'sqrt', 1, + Primitive(Primitive.square_root, return_type=TYPE_FLOAT, + arg_descs=[ArgSlot(TYPE_NUMBER)])) - primitive_dictionary['random'] = self._prim_random palette.add_block('random', style='number-style-block', label=[_('random'), _('min'), _('max')], @@ -703,9 +752,19 @@ blocks')) logo_command='tarandom', help_string=_('returns random number between \ minimum (top) and maximum (bottom) values')) + self.tw.lc.def_prim( - 'random', 2, lambda self, x, y: primitive_dictionary['random']( - x, y)) + 'random', 2, + or_( # random character ... + Primitive(Primitive.random_char, return_type=TYPE_CHAR, + arg_descs=[ + ArgSlot(TYPE_INT, + wrapper=self.prim_cache["ord"]), + ArgSlot(TYPE_INT, + wrapper=self.prim_cache["ord"])]), + # ... or random number + Primitive(Primitive.random_int, return_type=TYPE_INT, + arg_descs=[ArgSlot(TYPE_INT), ArgSlot(TYPE_INT)]))) define_logo_function('tarandom', 'to tarandom :min :max\n \ output (random (:max - :min)) + :min\nend\n') @@ -717,7 +776,6 @@ output (random (:max - :min)) + :min\nend\n') help_string=_('used as numeric input in mathematic \ operators')) - primitive_dictionary['more'] = self._prim_more palette.add_block('greater2', style='compare-porch-style', label=' >', @@ -728,9 +786,16 @@ operators')) help_string=_('logical greater-than operator')) self.tw.lc.def_prim( 'greater?', 2, - lambda self, x, y: primitive_dictionary['more'](x, y)) + Primitive(Primitive.greater, return_type=TYPE_BOOL, + arg_descs=or_([ArgSlot(TYPE_COLOR), + ArgSlot(TYPE_COLOR)], + [ArgSlot(TYPE_NUMBER), + ArgSlot(TYPE_NUMBER)], + [ArgSlot(TYPE_STRING), + ArgSlot(TYPE_STRING)], + [ArgSlot(TYPE_OBJECT), + ArgSlot(TYPE_OBJECT)]))) - primitive_dictionary['less'] = self._prim_less palette.add_block('less2', style='compare-porch-style', label=' <', @@ -740,9 +805,17 @@ operators')) logo_command='less?', help_string=_('logical less-than operator')) self.tw.lc.def_prim( - 'less?', 2, lambda self, x, y: primitive_dictionary['less'](x, y)) + 'less?', 2, + Primitive(Primitive.less, return_type=TYPE_BOOL, + arg_descs=or_([ArgSlot(TYPE_COLOR), + ArgSlot(TYPE_COLOR)], + [ArgSlot(TYPE_NUMBER), + ArgSlot(TYPE_NUMBER)], + [ArgSlot(TYPE_STRING), + ArgSlot(TYPE_STRING)], + [ArgSlot(TYPE_OBJECT), + ArgSlot(TYPE_OBJECT)]))) - primitive_dictionary['equal'] = self._prim_equal palette.add_block('equal2', style='compare-style', label='=', @@ -751,9 +824,17 @@ operators')) prim_name='equal?', logo_command='equal?', help_string=_('logical equal-to operator')) - self.tw.lc.def_prim('equal?', 2, - lambda self, x, y: - primitive_dictionary['equal'](x, y)) + self.tw.lc.def_prim( + 'equal?', 2, + Primitive(Primitive.equals, return_type=TYPE_BOOL, + arg_descs=or_([ArgSlot(TYPE_COLOR), + ArgSlot(TYPE_COLOR)], + [ArgSlot(TYPE_NUMBER), + ArgSlot(TYPE_NUMBER)], + [ArgSlot(TYPE_STRING), + ArgSlot(TYPE_STRING)], + [ArgSlot(TYPE_OBJECT), + ArgSlot(TYPE_OBJECT)]))) palette.add_block('not', style='not-style', @@ -761,9 +842,11 @@ operators')) prim_name='not', logo_command='not', help_string=_('logical NOT operator')) - self.tw.lc.def_prim('not', 1, lambda self, x: not x) + self.tw.lc.def_prim( + 'not', 1, + Primitive(Primitive.not_, return_type=TYPE_BOOL, + arg_descs=[ArgSlot(TYPE_BOOL)])) - primitive_dictionary['and'] = self._prim_and palette.add_block('and2', style='boolean-style', label=_('and'), @@ -772,9 +855,10 @@ operators')) special_name=_('and'), help_string=_('logical AND operator')) self.tw.lc.def_prim( - 'and', 2, lambda self, x, y: primitive_dictionary['and'](x, y)) + 'and', 2, + Primitive(Primitive.and_, return_type=TYPE_BOOL, + arg_descs=[ArgSlot(TYPE_BOOL), ArgSlot(TYPE_BOOL)])) - primitive_dictionary['or'] = self._prim_or palette.add_block('or2', style='boolean-style', label=_('or'), @@ -783,18 +867,18 @@ operators')) special_name=_('or'), help_string=_('logical OR operator')) self.tw.lc.def_prim( - 'or', 2, lambda self, x, y: primitive_dictionary['or'](x, y)) + 'or', 2, + Primitive(Primitive.or_, return_type=TYPE_BOOL, + arg_descs=[ArgSlot(TYPE_BOOL), ArgSlot(TYPE_BOOL)])) def _flow_palette(self): ''' The basic Turtle Art flow palette ''' - debug_output('creating %s palette' % _('flow'), - self.tw.running_sugar) palette = make_palette('flow', colors=["#FFC000", "#A08000"], - help_string=_('Palette of flow operators')) + help_string=_('Palette of flow operators'), + translation=_('flow')) - primitive_dictionary['wait'] = self._prim_wait palette.add_block('wait', style='basic-style-1arg', label=_('wait'), @@ -803,54 +887,74 @@ operators')) logo_command='wait', help_string=_('pauses program execution a specified \ number of seconds')) - self.tw.lc.def_prim('wait', 1, primitive_dictionary['wait'], True) + self.tw.lc.def_prim( + 'wait', 1, + Primitive(self.tw.lc.prim_wait, arg_descs=[ArgSlot(TYPE_NUMBER)]), + True) - primitive_dictionary['forever'] = self._prim_forever palette.add_block('forever', style='clamp-style', label=_('forever'), prim_name='forever', - default=[None, None], + default=[None], logo_command='forever', help_string=_('loops forever')) - self.tw.lc.def_prim('forever', 1, primitive_dictionary['forever'], - True) + self.tw.lc.def_prim( + 'forever', 1, + Primitive(self.tw.lc.prim_loop, arg_descs=[ConstantArg(Primitive( + Primitive.controller_forever)), ArgSlot(TYPE_OBJECT, + call_arg=False)]), + True) - primitive_dictionary['repeat'] = self._prim_repeat palette.add_block('repeat', style='clamp-style-1arg', label=_('repeat'), prim_name='repeat', - default=[4, None, None], + default=[4, None], logo_command='repeat', special_name=_('repeat'), help_string=_('loops specified number of times')) - self.tw.lc.def_prim('repeat', 2, primitive_dictionary['repeat'], True) + self.tw.lc.def_prim( + 'repeat', 2, + Primitive(self.tw.lc.prim_loop, + arg_descs=[ArgSlot( + TYPE_OBJECT, + wrapper=Primitive(Primitive.controller_repeat, + arg_descs=[ArgSlot(TYPE_INT)])), + ArgSlot(TYPE_OBJECT, call_arg=False)]), + True) - primitive_dictionary['if'] = self._prim_if palette.add_block('if', style='clamp-style-boolean', label=[_('if'), _('then'), ''], prim_name='if', - default=[None, None, None], + default=[None, None], special_name=_('if then'), logo_command='if', help_string=_('if-then operator that uses boolean \ operators from Numbers palette')) - self.tw.lc.def_prim('if', 2, primitive_dictionary['if'], True) + self.tw.lc.def_prim( + 'if', 2, + Primitive(self.tw.lc.prim_if, + arg_descs=[ArgSlot(TYPE_BOOL), ArgSlot(TYPE_OBJECT)]), + True) - primitive_dictionary['ifelse'] = self._prim_ifelse palette.add_block('ifelse', hidden=True, # Too big to fit palette style='clamp-style-else', label=[_('if'), _('then'), _('else')], prim_name='ifelse', - default=[None, None, None, None], + default=[None, None, None], logo_command='ifelse', special_name=_('if then else'), help_string=_('if-then-else operator that uses \ boolean operators from Numbers palette')) - self.tw.lc.def_prim('ifelse', 3, primitive_dictionary['ifelse'], True) + self.tw.lc.def_prim( + 'ifelse', 3, + Primitive(self.tw.lc.prim_ifelse, + arg_descs=[ArgSlot(TYPE_BOOL), ArgSlot(TYPE_OBJECT), + ArgSlot(TYPE_OBJECT)]), + True) # macro palette.add_block('ifthenelse', @@ -867,7 +971,9 @@ boolean operators from Numbers palette')) prim_name='nop', special_name=_('horizontal space'), help_string=_('jogs stack right')) - self.tw.lc.def_prim('nop', 0, lambda self: None) + self.tw.lc.def_prim( + 'nop', 0, + Primitive(Primitive.do_nothing, export_me=False)) palette.add_block('vspace', style='basic-style-extended-vertical', @@ -875,28 +981,28 @@ boolean operators from Numbers palette')) prim_name='nop', special_name=_('vertical space'), help_string=_('jogs stack down')) - self.tw.lc.def_prim('nop', 0, lambda self: None) + self.tw.lc.def_prim( + 'nop', 0, + Primitive(Primitive.do_nothing, export_me=False)) - primitive_dictionary['stopstack'] = self._prim_stopstack palette.add_block('stopstack', style='basic-style-tail', label=_('stop action'), prim_name='stopstack', logo_command='stop', help_string=_('stops current action')) - self.tw.lc.def_prim('stopstack', 0, - lambda self: primitive_dictionary['stopstack']()) + self.tw.lc.def_prim( + 'stopstack', 0, + Primitive(self.tw.lc.prim_stop_stack)) def _blocks_palette(self): ''' The basic Turtle Art blocks palette ''' - debug_output('creating %s palette' % _('blocks'), - self.tw.running_sugar) palette = make_palette('blocks', colors=["#FFFF00", "#A0A000"], - help_string=_('Palette of variable blocks')) + help_string=_('Palette of variable blocks'), + translation=_('blocks')) - primitive_dictionary['start'] = self._prim_start palette.add_block('start', style='basic-style-head', label=_('start'), @@ -904,8 +1010,13 @@ boolean operators from Numbers palette')) logo_command='to start\n', help_string=_('connects action to toolbar run \ buttons')) - self.tw.lc.def_prim('start', 0, - lambda self: primitive_dictionary['start']()) + self.tw.lc.def_prim( + 'start', 0, + Primitive(Primitive.group, arg_descs=[ConstantArg([ + Primitive(self.tw.lc.prim_start, + export_me=False), + Primitive(self.tw.lc.prim_define_stack, + arg_descs=[ConstantArg('start')])])])) palette.add_block('string', style='box-style', @@ -922,9 +1033,14 @@ buttons')) default=_('action'), logo_command='to action', help_string=_('top of nameable action stack')) - self.tw.lc.def_prim('nop3', 1, lambda self, x: None) + self.tw.lc.def_prim( + 'nop3', 1, + Primitive(self.tw.lc.prim_define_stack, + arg_descs=[ArgSlot(TYPE_OBJECT)])) - primitive_dictionary['stack'] = self._prim_stack + primitive_dictionary['stack'] = Primitive( + self.tw.lc.prim_invoke_stack, + arg_descs=[ArgSlot(TYPE_OBJECT)]) palette.add_block('stack', style='basic-style-1arg', label=_('action'), @@ -933,9 +1049,9 @@ buttons')) logo_command='action', default=_('action'), help_string=_('invokes named action stack')) - self.tw.lc.def_prim('stack', 1, primitive_dictionary['stack'], True) + self.tw.lc.def_prim('stack', 1, + primitive_dictionary['stack'], True) - primitive_dictionary['setbox'] = self._prim_setbox palette.add_block('storeinbox1', hidden=True, style='basic-style-1arg', @@ -945,10 +1061,10 @@ buttons')) string_or_number=True, logo_command='make "box1', help_string=_('stores numeric value in Variable 1')) - self.tw.lc.def_prim('storeinbox1', 1, - lambda self, x: - primitive_dictionary['setbox'] - ('box1', None, x)) + self.tw.lc.def_prim( + 'storeinbox1', 1, + Primitive(self.tw.lc.prim_set_box, + arg_descs=[ConstantArg('box1'), ArgSlot(TYPE_OBJECT)])) palette.add_block('storeinbox2', hidden=True, @@ -959,10 +1075,10 @@ buttons')) string_or_number=True, logo_command='make "box2', help_string=_('stores numeric value in Variable 2')) - self.tw.lc.def_prim('storeinbox2', 1, - lambda self, x: - primitive_dictionary['setbox'] - ('box2', None, x)) + self.tw.lc.def_prim( + 'storeinbox2', 1, + Primitive(self.tw.lc.prim_set_box, + arg_descs=[ConstantArg('box2'), ArgSlot(TYPE_OBJECT)])) palette.add_block('box1', hidden=True, @@ -972,7 +1088,10 @@ buttons')) logo_command=':box1', help_string=_('Variable 1 (numeric value)'), value_block=True) - self.tw.lc.def_prim('box1', 0, lambda self: self.tw.lc.boxes['box1']) + self.tw.lc.def_prim( + 'box1', 0, + Primitive(self.tw.lc.prim_get_box, return_type=TYPE_BOX, + arg_descs=[ConstantArg('box1')])) palette.add_block('box2', hidden=True, @@ -982,8 +1101,14 @@ buttons')) logo_command=':box2', help_string=_('Variable 2 (numeric value)'), value_block=True) - self.tw.lc.def_prim('box2', 0, lambda self: self.tw.lc.boxes['box2']) + self.tw.lc.def_prim( + 'box2', 0, + Primitive(self.tw.lc.prim_get_box, return_type=TYPE_BOX, + arg_descs=[ConstantArg('box2')])) + primitive_dictionary['setbox'] = Primitive( + self.tw.lc.prim_set_box, + arg_descs=[ArgSlot(TYPE_OBJECT), ArgSlot(TYPE_OBJECT)]) palette.add_block('storein', style='basic-style-2arg', label=[_('store in'), _('box'), _('value')], @@ -993,12 +1118,12 @@ buttons')) default=[_('my box'), 100], help_string=_('stores numeric value in named \ variable')) - self.tw.lc.def_prim('storeinbox', 2, - lambda self, x, y: - primitive_dictionary['setbox'] - ('box3', x, y)) + self.tw.lc.def_prim('storeinbox', 2, primitive_dictionary['setbox']) - primitive_dictionary['box'] = self._prim_box + primitive_dictionary['box'] = Primitive( + self.tw.lc.prim_get_box, + return_type=TYPE_BOX, + arg_descs=[ArgSlot(TYPE_OBJECT)]) palette.add_block('box', style='number-style-1strarg', hidden=True, @@ -1009,8 +1134,7 @@ variable')) logo_command='box', value_block=True, help_string=_('named variable (numeric value)')) - self.tw.lc.def_prim('box', 1, - lambda self, x: primitive_dictionary['box'](x)) + self.tw.lc.def_prim('box', 1, primitive_dictionary['box']) palette.add_block('hat1', hidden=True, @@ -1019,7 +1143,10 @@ variable')) prim_name='nop1', logo_command='to stack1\n', help_string=_('top of Action 1 stack')) - self.tw.lc.def_prim('nop1', 0, lambda self: None) + self.tw.lc.def_prim( + 'nop1', 0, + Primitive(self.tw.lc.prim_define_stack, + arg_descs=[ConstantArg('stack1')])) palette.add_block('hat2', hidden=True, @@ -1028,9 +1155,11 @@ variable')) prim_name='nop2', logo_command='to stack2\n', help_string=_('top of Action 2 stack')) - self.tw.lc.def_prim('nop2', 0, lambda self: None) + self.tw.lc.def_prim( + 'nop2', 0, + Primitive(self.tw.lc.prim_define_stack, + arg_descs=[ConstantArg('stack2')])) - primitive_dictionary['stack1'] = self._prim_stack1 palette.add_block('stack1', hidden=True, style='basic-style-extended-vertical', @@ -1038,9 +1167,12 @@ variable')) prim_name='stack1', logo_command='stack1', help_string=_('invokes Action 1 stack')) - self.tw.lc.def_prim('stack1', 0, primitive_dictionary['stack1'], True) + self.tw.lc.def_prim( + 'stack1', 0, + Primitive(self.tw.lc.prim_invoke_stack, + arg_descs=[ConstantArg('stack1')]), + True) - primitive_dictionary['stack2'] = self._prim_stack2 palette.add_block('stack2', hidden=True, style='basic-style-extended-vertical', @@ -1048,16 +1180,19 @@ variable')) prim_name='stack2', logo_command='stack2', help_string=_('invokes Action 2 stack')) - self.tw.lc.def_prim('stack2', 0, primitive_dictionary['stack2'], True) + self.tw.lc.def_prim( + 'stack2', 0, + Primitive(self.tw.lc.prim_invoke_stack, + arg_descs=[ConstantArg('stack2')]), + True) def _trash_palette(self): ''' The basic Turtle Art turtle palette ''' - debug_output('creating %s palette' % _('trash'), - self.tw.running_sugar) palette = make_palette('trash', colors=["#FFFF00", "#A0A000"], - help_string=_('trash')) + help_string=_('trash'), + translation=_('trash')) palette.add_block('empty', style='blank-style', @@ -1074,19 +1209,9 @@ variable')) label=_('clear all'), help_string=_('move all blocks to trash')) - # Block primitives - - def _prim_clear(self): - self.tw.lc.prim_clear() - self.tw.turtles.reset_turtles() - - def _prim_and(self, x, y): - ''' Logical and ''' - return x & y + # Callbacks to update labels after executing a block - def _prim_arc(self, cmd, value1, value2): - ''' Turtle draws an arc of degree, radius ''' - cmd(float(value1), float(value2)) + def after_arc(self, *ignored_args): if self.tw.lc.update_values: self.tw.lc.update_label_value( 'xcor', @@ -1100,65 +1225,8 @@ variable')) 'heading', self.tw.turtles.get_active_turtle().get_heading()) - def _prim_box(self, x): - ''' Retrieve value from named box ''' - if isinstance(convert(x, float, False), float): - if int(float(x)) == x: - x = int(x) - try: - return self.tw.lc.boxes['box3' + str(x)] - except KeyError: - raise logoerror("#emptybox") - - def _prim_forever(self, blklist): - ''' Do list forever ''' - while True: - self.tw.lc.icall(self.tw.lc.evline, blklist[:]) - yield True - if self.tw.lc.procstop: - break - self.tw.lc.ireturn() - yield True - - def _prim_if(self, boolean, blklist): - ''' If bool, do list ''' - if boolean: - self.tw.lc.icall(self.tw.lc.evline, blklist[:]) - yield True - self.tw.lc.ireturn() - yield True - - def _prim_ifelse(self, boolean, list1, list2): - ''' If bool, do list1, else do list2 ''' - if boolean: - self.tw.lc.ijmp(self.tw.lc.evline, list1[:]) - yield True - else: - self.tw.lc.ijmp(self.tw.lc.evline, list2[:]) - yield True - - def _prim_move(self, cmd, value1, value2=None, pendown=True, - reverse=False): - ''' Turtle moves by method specified in value1 ''' - pos = None - if isinstance(value1, (tuple, list)): - pos = value1 - value1 = pos[0] - value2 = pos[1] - if not _num_type(value1): - raise logoerror("#notanumber") - if value2 is None: - if reverse: - cmd(float(-value1)) - else: - cmd(float(value1)) - else: - if not _num_type(value2): - raise logoerror("#notanumber") - if pos is not None: - cmd((float(value1), float(value2)), pendown=pendown) - else: - cmd(float(value1), float(value2), pendown=pendown) + def after_move(self, *ignored_args, **ignored_kwargs): + ''' Update labels after moving the turtle ''' if self.tw.lc.update_values: self.tw.lc.update_label_value( 'xcor', @@ -1169,323 +1237,38 @@ variable')) self.tw.turtles.get_active_turtle().get_xy()[1] / self.tw.coord_scale) - def _prim_or(self, x, y): - ''' Logical or ''' - return x | y - - def _prim_repeat(self, num, blklist): - ''' Repeat list num times. ''' - if not _num_type(num): - raise logoerror("#notanumber") - num = self.tw.lc.int(num) - for i in range(num): - self.tw.lc.icall(self.tw.lc.evline, blklist[:]) - yield True - if self.tw.lc.procstop: - break - self.tw.lc.ireturn() - yield True - - def _prim_right(self, value, reverse=False): - ''' Turtle rotates clockwise ''' - if not _num_type(value): - raise logoerror("#notanumber") - if reverse: - self.tw.turtles.get_active_turtle().right(float(-value)) - else: - self.tw.turtles.get_active_turtle().right(float(value)) + def after_right(self, *ignored_args): if self.tw.lc.update_values: self.tw.lc.update_label_value( 'heading', self.tw.turtles.get_active_turtle().get_heading()) - def _prim_set(self, name, cmd, value=None): - ''' Set a value and update the associated value blocks ''' + def after_set(self, name, value=None): + ''' Update the associated value blocks ''' if value is not None: - cmd(value) if self.tw.lc.update_values: self.tw.lc.update_label_value(name, value) - def _prim_setbox(self, name, x, val): - ''' Define value of named box ''' - if x is not None: - if isinstance(convert(x, float, False), float): - if int(float(x)) == x: - x = int(x) - self.tw.lc.boxes[name + str(x)] = val - if self.tw.lc.update_values: - self.tw.lc.update_label_value('box', val, label=x) - else: - self.tw.lc.boxes[name] = val - if self.tw.lc.update_values: - self.tw.lc.update_label_value(name, val) - - def _prim_stack(self, x): - ''' Process a named stack ''' - if isinstance(convert(x, float, False), float): - if int(float(x)) == x: - x = int(x) - if 'stack3' + str(x) not in self.tw.lc.stacks or \ - self.tw.lc.stacks['stack3' + str(x)] is None: - raise logoerror("#nostack") - self.tw.lc.icall(self.tw.lc.evline, - self.tw.lc.stacks['stack3' + str(x)][:]) - yield True - self.tw.lc.procstop = False - self.tw.lc.ireturn() - yield True - - def _prim_stack1(self): - ''' Process Stack 1 ''' - if self.tw.lc.stacks['stack1'] is None: - raise logoerror("#nostack") - self.tw.lc.icall(self.tw.lc.evline, - self.tw.lc.stacks['stack1'][:]) - yield True - self.tw.lc.procstop = False - self.tw.lc.ireturn() - yield True - - def _prim_stack2(self): - ''' Process Stack 2 ''' - if self.tw.lc.stacks['stack2'] is None: - raise logoerror("#nostack") - self.tw.lc.icall(self.tw.lc.evline, self.tw.lc.stacks['stack2'][:]) - yield True - self.tw.lc.procstop = False - self.tw.lc.ireturn() - yield True - - def _prim_start(self): - ''' Start block: recenter ''' - if self.tw.running_sugar: - self.tw.activity.recenter() - - def _prim_stopstack(self): - ''' Stop execution of a stack ''' - self.tw.lc.procstop = True - - def _prim_wait(self, wait_time): - ''' Show the turtle while we wait ''' - self.tw.turtles.get_active_turtle().show() - endtime = _millisecond() + wait_time * 1000. - while _millisecond() < endtime: - sleep(wait_time / 10.) - yield True - self.tw.turtles.get_active_turtle().hide() - self.tw.lc.ireturn() - yield True - - # Math primitivies - - def _prim_careful_divide(self, x, y): - ''' Raise error on divide by zero ''' - if isinstance(x, list) and _num_type(y): - z = [] - for i in range(len(x)): - try: - z.append(x[i] / y) - except ZeroDivisionError: - raise logoerror("#zerodivide") - return z - try: - return x / y - except ZeroDivisionError: - raise logoerror("#zerodivide") - except TypeError: - try: - return self._string_to_num(x) / self._string_to_num(y) - except ZeroDivisionError: - raise logoerror("#zerodivide") - except ValueError: - raise logoerror("#syntaxerror") - except TypeError: - raise logoerror("#notanumber") - - def _prim_equal(self, x, y): - ''' Numeric and logical equal ''' - if isinstance(x, list) and isinstance(y, list): - for i in range(len(x)): - if x[i] != y[i]: - return False - return True - try: - return float(x) == float(y) - except ValueError: - typex, typey = False, False - if strtype(x): - typex = True - if strtype(y): - typey = True - if typex and typey: - return x == y - try: - return self._string_to_num(x) == self._string_to_num(y) - except TypeError: - raise logoerror("#syntaxerror") - - def _prim_less(self, x, y): - ''' Compare numbers and strings ''' - if isinstance(x, list) or isinstance(y, list): - raise logoerror("#syntaxerror") - try: - return float(x) < float(y) - except ValueError: - typex, typey = False, False - if strtype(x): - typex = True - if strtype(y): - typey = True - if typex and typey: - return x < y - try: - return self._string_to_num(x) < self._string_to_num(y) - except TypeError: - raise logoerror("#notanumber") - - def _prim_more(self, x, y): - ''' Compare numbers and strings ''' - return self._prim_less(y, x) - - def _prim_plus(self, x, y): - ''' Add numbers, concat strings ''' - if x in COLORDICT: - x = _color_to_num(x) - if y in COLORDICT: - y = _color_to_num(y) - if _num_type(x) and _num_type(y): - return(x + y) - elif isinstance(x, list) and isinstance(y, list): - z = [] - for i in range(len(x)): - z.append(x[i] + y[i]) - return(z) - else: - if _num_type(x): - xx = str(round_int(x)) - else: - xx = str(x) - if _num_type(y): - yy = str(round_int(y)) - else: - yy = str(y) - return(xx + yy) - - def _prim_minus(self, x, y): - ''' Numerical subtraction ''' - if _num_type(x) and _num_type(y): - return(x - y) - elif isinstance(x, list) and isinstance(y, list): - z = [] - for i in range(len(x)): - z.append(x[i] - y[i]) - return(z) - try: - return self._string_to_num(x) - self._string_to_num(y) - except TypeError: - raise logoerror("#notanumber") - - def _prim_product(self, x, y): - ''' Numerical multiplication ''' - if _num_type(x) and _num_type(y): - return(x * y) - elif isinstance(x, list) and _num_type(y): - z = [] - for i in range(len(x)): - z.append(x[i] * y) - return(z) - elif isinstance(y, list) and _num_type(x): - z = [] - for i in range(len(y)): - z.append(y[i] * x) - return(z) - try: - return self._string_to_num(x) * self._string_to_num(y) - except TypeError: - raise logoerror("#notanumber") - - def _prim_mod(self, x, y): - ''' Numerical mod ''' - if _num_type(x) and _num_type(y): - return(x % y) - try: - return self._string_to_num(x) % self._string_to_num(y) - except TypeError: - raise logoerror("#notanumber") - except ValueError: - raise logoerror("#syntaxerror") - - def _prim_sqrt(self, x): - ''' Square root ''' - if _num_type(x): - if x < 0: - raise logoerror("#negroot") - return sqrt(x) - try: - return sqrt(self._string_to_num(x)) - except ValueError: - raise logoerror("#negroot") - except TypeError: - raise logoerror("#notanumber") - - def _prim_random(self, x, y): - ''' Random integer ''' - if _num_type(x) and _num_type(y): - return(int(round(uniform(x, y), 0))) - xx, xflag = chr_to_ord(x) - yy, yflag = chr_to_ord(y) - if xflag and yflag: - return chr(int(round(uniform(xx, yy), 0))) - if not xflag: - xx = self._string_to_num(x) - if not yflag: - yy = self._string_to_num(y) - try: - return(int(round(uniform(xx, yy), 0))) - except TypeError: - raise logoerror("#notanumber") - - def _prim_identity(self, x): - ''' Identity function ''' - return(x) - # Utilities - def _string_to_num(self, x): - ''' Try to comvert a string to a number ''' - if isinstance(x, (int, float)): - return(x) - try: - return int(ord(x)) - except TypeError: - pass - if isinstance(x, list): - raise logoerror("#syntaxerror") - if x in COLORDICT: - return _color_to_num(x) - xx = convert(x.replace(self.tw.decimal_point, '.'), float) - if isinstance(xx, float): - return xx - else: - xx, xflag = chr_to_ord(x) - if xflag: - return xx - else: - raise logoerror("#syntaxerror") - - def _make_constant(self, palette, block_name, label, constant): + def _make_constant(self, palette, block_name, label, constant_key): ''' Factory for constant blocks ''' - if constant in COLORDICT: - if COLORDICT[constant][0] is not None: - value = str(COLORDICT[constant][0]) + constant = CONSTANTS[constant_key] + if isinstance(constant, Color): + if constant.color is not None: + logo_command = str(constant.color) else: # Black or White - value = '0 tasetshade %d' % (COLORDICT[constant][1]) + logo_command = '0 tasetshade %d' % (constant.shade) + return_type = TYPE_COLOR else: - value = constant + logo_command = constant + return_type = TYPE_NUMBER palette.add_block(block_name, style='box-style', label=label, prim_name=block_name, - logo_command=value) - self.tw.lc.def_prim(block_name, 0, lambda self: constant) + logo_command=logo_command) + self.tw.lc.def_prim(block_name, 0, + Primitive(CONSTANTS.get, return_type=return_type, + arg_descs=[ConstantArg(constant_key)])) diff --git a/TurtleArt/tablock.py b/TurtleArt/tablock.py index ff392e0..81bdd2a 100644 --- a/TurtleArt/tablock.py +++ b/TurtleArt/tablock.py @@ -24,7 +24,8 @@ import cairo from taconstants import (EXPANDABLE, EXPANDABLE_ARGS, OLD_NAMES, CONSTANTS, STANDARD_STROKE_WIDTH, BLOCK_SCALE, BOX_COLORS, - GRADIENT_COLOR, EXPANDABLE_FLOW, COLORDICT) + GRADIENT_COLOR, EXPANDABLE_FLOW, Color, + MEDIA_BLOCK2TYPE, BLOCKS_WITH_SKIN) from tapalette import (palette_blocks, block_colors, expandable_blocks, content_blocks, block_names, block_primitives, block_styles, special_block_colors) @@ -34,6 +35,36 @@ import sprites from tautils import (debug_output, error_output) +media_blocks_dictionary = {} # new media blocks get added here + +class Media(object): + """ Media objects can be images, audio files, videos, Journal + descriptions, or camera snapshots. """ + + ALL_TYPES = ('media', 'audio', 'video', 'descr', 'camera', 'camera1') + + def __init__(self, type_, value=None): + """ + type_ --- a string that indicates the kind of media: + media --- image + audio --- audio file + video --- video + descr --- Journal description + camera, camera1 --- camera snapshot + value --- a file path or a reference to a Sugar datastore object """ + if type_ not in Media.ALL_TYPES: + raise ValueError("Media.type must be one of " + + repr(Media.ALL_TYPES)) + self.type = type_ + self.value = value + + def __str__(self): + return '%s_%s' % (self.type, str(self.value)) + + def __repr__(self): + return 'Media(type=%s, value=%s)' % (repr(self.type), repr(self.value)) + + class Blocks: """ A class for the list of blocks and everything they share in common """ @@ -152,7 +183,7 @@ class Block: trash -- block in the trash """ def __init__(self, block_list, sprite_list, name, x, y, type='block', - values=[], scale=BLOCK_SCALE[0], + values=None, scale=BLOCK_SCALE[0], colors=['#A0A0A0', '#808080']): self.block_list = block_list @@ -178,7 +209,7 @@ class Block: self._visible = True self.unknown = False # Block is of unknown style - self.block_methods = { + self._block_methods = { 'basic-style': self._make_basic_style, 'blank-style': self._make_blank_style, 'basic-style-head': self._make_basic_style_head, @@ -210,6 +241,7 @@ class Block: 'clamp-style-collapsed': self._make_clamp_style_collapsed, 'clamp-style-1arg': self._make_clamp_style_1arg, 'clamp-style-boolean': self._make_clamp_style_boolean, + 'clamp-style-until': self._make_clamp_style_until, 'clamp-style-else': self._make_clamp_style_else, 'flow-style-tail': self._make_flow_style_tail, 'portfolio-style-2x2': self._make_portfolio_style_2x2, @@ -224,8 +256,9 @@ class Block: self.font_size[i] *= self.scale * \ self.block_list.font_scale_factor - for v in (values): - self.values.append(v) + if values is not None: + for v in (values): + self.values.append(v) # If there is already a block with the same name, reuse it copy_block = None @@ -241,6 +274,13 @@ class Block: self.block_list.append_to_list(self) + def __repr__(self): + if self.is_value_block(): + name = self.get_value() + else: + name = self.name + return 'Block(%s)' % (repr(name)) + def get_visibility(self): ''' Should block be visible on the palette? ''' return self._visible @@ -277,6 +317,44 @@ class Block: return False return True + def is_value_block(self): + """ Return True iff this block is a value block (numeric, string, + media, etc.) """ + return self.primitive is None and self.values + + def get_value(self, add_type_prefix=True): + """ Return the value stored in this value block or None if this is + not a value block + add_type_prefix -- prepend a prefix to indicate the type of the + 'raw' value """ + if not self.is_value_block(): + return None + + if self.name == 'number': + try: + return float(self.values[0]) + except ValueError: + return float(ord(self.values[0][0])) + elif (self.name == 'string' or + self.name == 'title'): # deprecated block + if add_type_prefix: + result = '#s' + else: + result = '' + if isinstance(self.values[0], (float, int)): + if int(self.values[0]) == self.values[0]: + self.values[0] = int(self.values[0]) + result += str(self.values[0]) + else: + result += self.values[0] + return result + elif self.name in MEDIA_BLOCK2TYPE: + return Media(MEDIA_BLOCK2TYPE[self.name], self.values[0]) + elif self.name in media_blocks_dictionary: + return Media('media', self.name.upper()) + else: + return None + def highlight(self): """ We may want to highlight a block... """ if self.spr is not None and self.status is not 'collapsed': @@ -529,13 +607,12 @@ class Block: else: self._set_labels(i, str(v)) elif self.type == 'block' and self.name in CONSTANTS: - if CONSTANTS[self.name] in COLORDICT: - v = COLORDICT[CONSTANTS[self.name]][0] - if v is None: - v = COLORDICT[CONSTANTS[self.name]][1] + if isinstance(CONSTANTS[self.name], Color): + v = int(CONSTANTS[self.name]) else: v = CONSTANTS[self.name] - self._set_labels(0, block_names[self.name][0] + ' = ' + str(v)) + if self.name not in BLOCKS_WITH_SKIN: + self._set_labels(0, block_names[self.name][0] + ' = ' + str(v)) elif self.name in block_names: for i, n in enumerate(block_names[self.name]): @@ -561,11 +638,11 @@ class Block: if n == 0: n = 1 # Force a scale to be set, even if there is no value. else: + n = 0 if self.name in block_names: n = len(block_names[self.name]) - else: + elif self.name not in BLOCKS_WITH_SKIN: debug_output('WARNING: unknown block name %s' % (self.name)) - n = 0 for i in range(n): if i > 0: size = int(self.font_size[1] + 0.5) @@ -604,7 +681,8 @@ class Block: y = self.docks[1][3] - int(int(self.font_size[0] * 1.3)) self.spr.set_label_attributes(int(self.font_size[0] + 0.5), True, 'right', y_pos=y, i=0) - elif self.name in block_styles['clamp-style-boolean']: + elif self.name in block_styles['clamp-style-boolean'] or \ + self.name in block_styles['clamp-style-until']: y = self.docks[1][3] - int(int(self.font_size[0] * 1.3)) self.spr.set_label_attributes(int(self.font_size[0] + 0.5), True, 'right', y_pos=y, i=0) @@ -634,19 +712,18 @@ class Block: self._right = 0 self._bottom = 0 self.svg.set_stroke_width(STANDARD_STROKE_WIDTH) - self.svg.clear_docks() if isinstance(self.name, unicode): self.name = self.name.encode('utf-8') for k in block_styles.keys(): if self.name in block_styles[k]: - if isinstance(self.block_methods[k], list): - self.block_methods[k][0](svg, self.block_methods[k][1], - self.block_methods[k][2]) + if isinstance(self._block_methods[k], list): + self._block_methods[k][0](svg, self._block_methods[k][1], + self._block_methods[k][2]) else: - self.block_methods[k](svg) + self._block_methods[k](svg) return error_output('ERROR: block type not found %s' % (self.name)) - self.block_methods['blank-style'](svg) + self._block_methods['blank-style'](svg) self.unknown = True def _set_colors(self, svg): @@ -973,7 +1050,7 @@ class Block: self._make_block_graphics(svg, self.svg.basic_block) self.docks = [['flow', True, self.svg.docks[0][0], self.svg.docks[0][1]], - ['unavailable', True, 0, self.svg.docks[0][1] + 10, '['], + ['flow', True, 0, self.svg.docks[0][1] + 10, '['], ['flow', False, self.svg.docks[1][0], self.svg.docks[1][1], ']']] @@ -1011,6 +1088,25 @@ class Block: ['flow', False, self.svg.docks[4][0], self.svg.docks[4][1], ']']] + def _make_clamp_style_until(self, svg, extend_x=0, extend_y=4): + self.svg.expand(self.dx + self.ex + extend_x, self.ey + extend_y, + 0, self.ey2) + self.svg.set_slot(True) + self.svg.set_tab(True) + self.svg.set_boolean(True) + self.svg.second_clamp(False) + self._make_block_graphics(svg, self.svg.clamp_until) + # Dock positions are flipped + self.docks = [['flow', True, self.svg.docks[0][0], + self.svg.docks[0][1]], + ['bool', False, self.svg.docks[3][0], + self.svg.docks[3][1]], + ['flow', False, self.svg.docks[1][0], + self.svg.docks[1][1], '['], + # Skip bottom of clamp + ['flow', False, self.svg.docks[4][0], + self.svg.docks[4][1], ']']] + def _make_clamp_style_else(self, svg, extend_x=0, extend_y=4): self.svg.expand(self.dx + self.ex + extend_x, self.ey + extend_y, self.dx + self.ex + extend_x, self.ey2 + extend_y) @@ -1119,6 +1215,7 @@ class Block: def _make_block_graphics(self, svg, function, arg=None): self._set_colors(svg) self.svg.set_gradient(True, GRADIENT_COLOR) + self.svg.clear_docks() if arg is None: pixbuf = svg_str_to_pixbuf(function()) else: @@ -1128,6 +1225,7 @@ class Block: self.shapes[0] = _pixbuf_to_cairo_surface(pixbuf, self.width, self.height) self.svg.set_gradient(False) + self.svg.clear_docks() if arg is None: pixbuf = svg_str_to_pixbuf(function()) else: diff --git a/TurtleArt/tacanvas.py b/TurtleArt/tacanvas.py index 89b8ed1..f37a3a4 100644 --- a/TurtleArt/tacanvas.py +++ b/TurtleArt/tacanvas.py @@ -1,4 +1,4 @@ -31#Copyright (c) 2007-8, Playful Invention Company. +#Copyright (c) 2007-8, Playful Invention Company. #Copyright (c) 2008-11, Walter Bender #Copyright (c) 2011 Collabora Ltd. @@ -28,7 +28,7 @@ import cairo import pangocairo from tautils import get_path -from taconstants import COLORDICT, TMP_SVG_PATH +from taconstants import Color, TMP_SVG_PATH def wrap100(n): @@ -208,19 +208,19 @@ class TurtleGraphics: save_rgb = self._fgrgb[:] # Special case for color blocks - if color in COLORDICT: - if COLORDICT[color][0] is None: - self._shade = COLORDICT[color][1] + if isinstance(color, Color): + if color.color is None: + self._shade = color.shade else: - self._color = COLORDICT[color][0] + self._color = color.color else: self._color = color - if shade in COLORDICT: - self._shade = COLORDICT[shade][1] + if isinstance(shade, Color): + self._shade = shade.shade else: self._shade = shade - if gray in COLORDICT: - self._gray = COLORDICT[gray][2] + if isinstance(gray, Color): + self._gray = gray.gray else: self._gray = gray @@ -305,10 +305,14 @@ class TurtleGraphics: ''' Draw text ''' def _draw_text(cr, label, x, y, size, width, scale, heading, rgb): + import textwrap + final_scale = int(size * scale) * pango.SCALE + label = str(label) + label = '\n'.join(textwrap.wrap(label, int(width / scale))) cc = pangocairo.CairoContext(cr) pl = cc.create_layout() fd = pango.FontDescription('Sans') - fd.set_size(int(size * scale) * pango.SCALE) + fd.set_size(final_scale) pl.set_font_description(fd) if isinstance(label, (str, unicode)): pl.set_text(label.replace('\0', ' ')) diff --git a/TurtleArt/taconstants.py b/TurtleArt/taconstants.py index 835209e..3e92956 100644 --- a/TurtleArt/taconstants.py +++ b/TurtleArt/taconstants.py @@ -79,20 +79,173 @@ XO4 = 'xo4' UNKNOWN = 'unknown' TMP_SVG_PATH = '/tmp/turtle_output.svg' +ARG_MUST_BE_NUMBER = ['product2', 'minus2', 'random', 'remainder2', 'forward', + 'back', 'left', 'right', 'arc', 'setxy2', 'setxy', + 'fillscreen', 'setscale', 'setpensize', 'wait', + 'setcolor', 'seth', 'setgray', 'setshade', 'string', + 'fillscreen2'] + +KEY_DICT = { + 'Left': 1, + 'KP_Left': 1, + 'Up': 2, + 'KP_Up': 2, + 'Right': 3, + 'KP_Right': 3, + 'Down': 4, + 'KP_Down': 4, + 'BackSpace': 8, + 'Tab': 9, + 'Return': 13, + 'Escape': 27, + 'space': 32, + ' ': 32, + 'exclam': 33, + 'quotedbl': 34, + 'numbersign': 35, + 'dollar': 36, + 'percent': 37, + 'ampersand': 38, + 'apostrophe': 39, + 'parenleft': 40, + 'parenright': 41, + 'asterisk': 42, + 'plus': 43, + 'comma': 44, + 'minus': 45, + 'period': 46, + 'slash': 47, + 'colon': 58, + 'semicolon': 59, + 'less': 60, + 'equal': 61, + 'greater': 62, + 'question': 63, + 'at': 64, + 'underscore': 95, + 'bracketleft': 91, + 'backslash': 92, + 'bracketright': 93, + 'asciicircum': 94, + 'grave': 96, + 'braceleft': 123, + 'bar': 124, + 'braceright': 125, + 'asciitilde': 126, + 'Delete': 127, + } +REVERSE_KEY_DICT = { + 1: _('left'), + 2: _('up'), + 3: _('right'), + 4: _('down'), + 8: _('backspace'), + 9: _('tab'), + # TRANS: enter is the name of the enter (or return) key + 13: _('enter'), + 27: 'esc', + # TRANS: space is the name of the space key + 32: _('space'), + 127: _('delete') + } + + +class Color(object): + """ A color used in block programs (e.g., as pen color). """ + + def __init__(self, name, color=0, shade=50, gray=100): + """ name -- a string with the name of the color, e.g., 'red' + color -- the hue (0-100, or None for white, gray, and black) + shade -- the lightness (0 is black, 100 is white) + gray -- the saturation (0 is gray, 100 is fully saturated) """ + self.name = name + self.color = color + self.shade = shade + self.gray = gray + + def __int__(self): + if self.color is None: + return int(self.shade) + else: + return int(self.color) + + def __float__(self): + return float(int(self)) + + def get_number_string(self): + return str(int(self)) + + def __str__(self): + return str(self.name) + + def __repr__(self): + return '%s (%s/%d/%d)' % (str(self.name), str(self.color), + self.shade, self.gray) + + def __eq__(self, other): + """ A Color is equivalent to + * another Color with the same color, shade, and gray values + * an integer, float, or long that equals int(self) """ + if isinstance(other, Color): + return (self.color == other.color and self.shade == other.shade + and self.gray == other.gray) + elif isinstance(other, (int, float, long)): + return int(self) == other + ## * a basestring that equals str(self) + #elif isinstance(other, basestring): + # return str(self) == other + else: + return False + + def __lt__(self, other): + """ A Color is less than + * another Color whose name appears earlier in the alphabet + * a number that is less than int(self) + * a string that appears before the underscore in the ASCII table """ + if isinstance(other, Color): + return str(self) < str(other) + elif isinstance(other, (int, float, long)): + return int(self) < other + elif isinstance(other, basestring): + return '_' + str(self) < other + else: + return False + + def __gt__(self, other): + """ A Color is greater than + * another Color whose name appears later in the alphabet + * a number that is greater than int(self) + * a string that appears after the underscore in the ASCII table """ + if isinstance(other, Color): + return str(self) > str(other) + elif isinstance(other, (int, float, long)): + return int(self) > other + elif isinstance(other, basestring): + return '_' + str(self) > other + else: + return False + + def is_gray(self): + """ Return True iff this color is white, gray, or black, i.e. if its + hue is not set or its saturation is zero. """ + return self.color is None or not self.gray + + + CONSTANTS = {'leftpos': None, 'toppos': None, 'rightpos': None, 'bottompos': None, 'width': None, 'height': None, - 'black': '_black', 'white': '_white', 'red': '_red', - 'orange': '_orange', 'yellow': '_yellow', 'green': '_green', - 'cyan': '_cyan', 'blue': '_blue', 'purple': '_purple', + 'black': Color('black', None, 0, 0), + 'white': Color('white', None, 100, 0), + 'red': Color('red', 0, 50, 100), + 'orange': Color('orange', 10, 50, 100), + 'yellow': Color('yellow', 20, 50, 100), + 'green': Color('green', 40, 50, 100), + 'cyan': Color('cyan', 50, 50, 100), + 'blue': Color('blue', 70, 50, 100), + 'purple': Color('purple', 90, 50, 100), 'titlex': None, 'titley': None, 'leftx': None, 'topy': None, 'rightx': None, 'bottomy': None} -COLORDICT = {'_black': [None, 0, 0], '_white': [None, 100, 0], - '_red': [0, 50, 100], '_orange': [10, 50, 100], - '_yellow': [20, 50, 100], '_green': [40, 50, 100], - '_cyan': [50, 50, 100], '_blue': [70, 50, 100], - '_purple': [90, 50, 100]} - # Blocks that are expandable EXPANDABLE_STYLE = ['boolean-style', 'compare-porch-style', 'compare-style', 'number-style-porch', 'number-style', 'basic-style-2arg', @@ -113,10 +266,10 @@ OLD_DOCK = ['and', 'or', 'plus', 'minus', 'division', 'product', 'remainder'] CONTENT_ARGS = ['show', 'showaligned', 'push', 'storein', 'storeinbox1', 'storeinbox2'] -PREFIX_DICTIONARY = {} +MEDIA_BLOCK2TYPE = {} # map media blocks to media types -# These blocks get a special skin -BLOCKS_WITH_SKIN = [] + +BLOCKS_WITH_SKIN = [] # These blocks get a special skin PYTHON_SKIN = [] @@ -134,7 +287,7 @@ OVERLAY_SHAPES = ['Cartesian', 'Cartesian_labeled', 'polar', 'metric'] STATUS_SHAPES = ['status', 'info', 'nostack', 'dupstack', 'noinput', 'emptyheap', 'emptybox', 'nomedia', 'nocode', 'overflowerror', 'negroot', 'syntaxerror', 'nofile', 'nojournal', 'zerodivide', - 'notanumber', 'incompatible', 'help', 'print'] + 'notanumber', 'incompatible', 'help', 'print', 'noconnection'] # Emulate Sugar toolbar when running from outside of Sugar TOOLBAR_SHAPES = ['hideshowoff', 'eraseron', 'run-fastoff', @@ -197,7 +350,7 @@ MACROS = { [5, ['number', '0.1'], 0, 0, [4, None]], [6, 'kbinput', 0, 0, [4, None]]], 'picturelist': - [[0, 'sandwichtop_no_label', 0, 0, [None, 1]], + [[0, ['sandwichclamp', 252], 0, 0, [None, 1, None]], [1, 'penup', 0, 0, [0, 2]], [2, 'setxy2', 0, 0, [1, 3, 4, 5]], [3, 'titlex', 0, 0, [2, None]], @@ -214,12 +367,11 @@ MACROS = { [14, 'pendown', 0, 0, [11, 15]], [15, 'setscale', 0, 0, [14, 16, 17]], [16, ['number', '67'], 0, 0, [15, None]], - [17, 'list', 0, 0, [15, 18, 19, 20]], + [17, 'list', 0, 0, [15, 18, 19, None]], [18, ['string', '∙ '], 0, 0, [17, None]], - [19, ['string', '∙ '], 0, 0, [17, None]], - [20, 'sandwichbottom', 0, 0, [17, None]]], + [19, ['string', '∙ '], 0, 0, [17, None]]], 'picture1x1a': - [[0, 'sandwichtop_no_label', 0, 0, [None, 1]], + [[0, ['sandwichclamp', 231], 0, 0, [None, 1, None]], [1, 'penup', 0, 0, [0, 2]], [2, 'setxy2', 0, 0, [1, 3, 4, 5]], [3, 'titlex', 0, 0, [2, None]], @@ -236,11 +388,10 @@ MACROS = { [14, 'pendown', 0, 0, [11, 15]], [15, 'setscale', 0, 0, [14, 16, 17]], [16, ['number', '90'], 0, 0, [15, None]], - [17, 'showaligned', 0, 0, [15, 18, 19]], - [18, 'journal', 0, 0, [17, None]], - [19, 'sandwichbottom', 0, 0, [17, None]]], + [17, 'showaligned', 0, 0, [15, 18, None]], + [18, 'journal', 0, 0, [17, None]]], 'picture2x2': - [[0, 'sandwichtop_no_label', 0, 0, [None, 1]], + [[0, ['sandwichclamp', 546], 0, 0, [None, 1, None]], [1, 'penup', 0, 0, [0, 2]], [2, 'setxy2', 0, 0, [1, 3, 4, 5]], [3, 'titlex', 0, 0, [2, None]], @@ -278,11 +429,10 @@ MACROS = { [35, 'rightx', 0, 0, [34, None]], [36, 'bottomy', 0, 0, [34, None]], [37, 'pendown', 0, 0, [34, 38]], - [38, 'showaligned', 0, 0, [37, 39, 40]], - [39, 'journal', 0, 0, [38, None]], - [40, 'sandwichbottom', 0, 0, [38, None]]], + [38, 'showaligned', 0, 0, [37, 39, None]], + [39, 'journal', 0, 0, [38, None]]], 'picture1x2': - [[0, 'sandwichtop_no_label', 0, 0, [None, 1]], + [[0, ['sandwichclamp', 546], 0, 0, [None, 1, None]], [1, 'penup', 0, 0, [0, 2]], [2, 'setxy2', 0, 0, [1, 3, 4, 5]], [3, 'titlex', 0, 0, [2, None]], @@ -320,11 +470,10 @@ MACROS = { [35, 'rightx', 0, 0, [34, None]], [36, 'bottomy', 0, 0, [34, None]], [37, 'pendown', 0, 0, [34, 38]], - [38, 'showaligned', 0, 0, [37, 39, 40]], - [39, 'description', 0, 0, [38, None]], - [40, 'sandwichbottom', 0, 0, [38, None]]], + [38, 'showaligned', 0, 0, [37, 39, None]], + [39, 'description', 0, 0, [38, None]]], 'picture2x1': - [[0, 'sandwichtop_no_label', 0, 0, [None, 1]], + [[0, ['sandwichclamp', 546], 0, 0, [None, 1, None]], [1, 'penup', 0, 0, [0, 2]], [2, 'setxy2', 0, 0, [1, 3, 4, 5]], [3, 'titlex', 0, 0, [2, None]], @@ -362,11 +511,10 @@ MACROS = { [35, 'rightx', 0, 0, [34, None]], [36, 'bottomy', 0, 0, [34, None]], [37, 'pendown', 0, 0, [34, 38]], - [38, 'showaligned', 0, 0, [37, 39, 40]], - [39, 'description', 0, 0, [38, None]], - [40, 'sandwichbottom', 0, 0, [38, None]]], + [38, 'showaligned', 0, 0, [37, 39, None]], + [39, 'description', 0, 0, [38, None]]], 'picture1x1': - [[0, 'sandwichtop_no_label', 0, 0, [None, 1]], + [[0, ['sandwichclamp', 336], 0, 0, [None, 1, None]], [1, 'penup', 0, 0, [0, 2]], [2, 'setxy2', 0, 0, [1, 3, 4, 5]], [3, 'titlex', 0, 0, [2, None]], @@ -390,9 +538,11 @@ MACROS = { [21, 'rightx', 0, 0, [20, None]], [22, 'topy', 0, 0, [20, None]], [23, 'pendown', 0, 0, [20, 24]], - [24, 'showaligned', 0, 0, [23, 25, 26]], - [25, 'description', 0, 0, [24, None]], - [26, 'sandwichbottom', 0, 0, [24, None]]], + [24, 'showaligned', 0, 0, [23, 25, None]], + [25, 'description', 0, 0, [24, None]]], 'reskin': [[0, 'skin', 0, 0, [None, 1, None]], + [1, 'journal', 0, 0, [0, None]]], + 'loadheapfromjournal': + [[0, 'loadheap', 0, 0, [None, 1, None]], [1, 'journal', 0, 0, [0, None]]]} diff --git a/TurtleArt/taexportpython.py b/TurtleArt/taexportpython.py new file mode 100644 index 0000000..60bf0c1 --- /dev/null +++ b/TurtleArt/taexportpython.py @@ -0,0 +1,287 @@ +#Copyright (c) 2013 Marion Zepf + +#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. + +""" Python export tool """ + +import ast +from gettext import gettext as _ +from os import linesep +import re +import traceback +import util.codegen as codegen + +#from ast_pprint import * # only used for debugging, safe to comment out + +from talogo import LogoCode +from taprimitive import (ast_yield_true, Primitive, PyExportError, + value_to_ast) +from tautils import (find_group, find_top_block, get_stack_name) +from tawindow import plugins_in_use + + +_SETUP_CODE_START = """\ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +_INSTALL_PATH = '/usr/share/sugar/activities/TurtleArt.activity' +_ALTERNATIVE_INSTALL_PATH = \ + '/usr/local/share/sugar/activities/TurtleArt.activity' + +import os, sys +paths = [] +paths.append('../%s.activity') +paths.append(os.path.expanduser('~') + '/Activities/%s.activity') +paths.append('/usr/share/sugar/activities/%s.activity') +paths.append('/usr/local/share/sugar/activities/%s.activity') + +flag = False +for path in paths: + for activity in ['TurtleBlocks', 'TurtleBots']: + p = path % activity + if os.path.exists(p): + flag = True + sys.path.insert(0, p) + +if not flag: + print 'This code require the Turtle Blocks/Bots activity to be installed.' + exit(1) + +from time import * +from random import uniform +from math import * + +from pyexported.window_setup import * + + +tw = get_tw() + +BOX = {} +ACTION = {} + + +""" +_SETUP_CODE_END = """\ + +if __name__ == '__main__': + tw.lc.start_time = time() + tw.lc.icall(start) + gobject.idle_add(tw.lc.doevalstep) + gtk.main() +""" +_ACTION_STACK_START = """\ +def %s(): +""" +_START_STACK_START_ADD = """\ + tw.start_plugins() + global_objects = tw.get_global_objects() +""" +_ACTION_STACK_PREAMBLE = """\ + turtles = tw.turtles + turtle = turtles.get_active_turtle() + canvas = tw.canvas + logo = tw.lc + +""" +_ACTION_STACK_END = """\ +ACTION["%s"] = %s +""" +# character that is illegal in a Python identifier +PAT_IDENTIFIER_ILLEGAL_CHAR = re.compile("[^A-Za-z0-9_]") + + +def save_python(tw): + """ Find all the action stacks and turn each into Python code """ + all_blocks = tw.just_blocks() + blocks_covered = set() + tops_of_stacks = [] + for block in all_blocks: + if block not in blocks_covered: + top = find_top_block(block) + tops_of_stacks.append(top) + block_stack = find_group(top) + blocks_covered.update(set(block_stack)) + + snippets = [_SETUP_CODE_START] + for block in tops_of_stacks: + stack_name = get_stack_name(block) + if stack_name: + pythoncode = _action_stack_to_python(block, tw, name=stack_name) + snippets.append(pythoncode) + snippets.append(linesep) + snippets.append(_SETUP_CODE_END) + return "".join(snippets) + + +def _action_stack_to_python(block, tw, name="start"): + """ Turn a stack of blocks into Python code + name -- the name of the action stack (defaults to "start") """ + + if isinstance(name, int): + name = float(name) + if not isinstance(name, basestring): + name = str(name) + + # traverse the block stack and get the AST for every block + ast_list = _walk_action_stack(block, tw.lc) + if not ast_list or not isinstance(ast_list[-1], ast.Yield): + ast_list.append(ast_yield_true()) + action_stack_ast = ast.Module(body=ast_list) + + # serialize the ASTs into python code + generated_code = codegen.to_source(action_stack_ast) + + # wrap the action stack setup code around everything + name_id = _make_identifier(name) + if name == 'start': + pre_preamble = _START_STACK_START_ADD + for k in plugins_in_use: + pre_preamble += " %s = global_objects['%s']\n" % (k.lower(), k) + else: + pre_preamble = '' + generated_code = _indent(generated_code, 1) + if generated_code.endswith(linesep): + newline = "" + else: + newline = linesep + snippets = [_ACTION_STACK_START % (name_id), + pre_preamble, + _ACTION_STACK_PREAMBLE, + generated_code, + newline, + _ACTION_STACK_END % (name, name_id)] + return "".join(snippets) + + +def _walk_action_stack(top_block, lc, convert_me=True): + """ Turn a stack of blocks into a list of ASTs + convert_me -- convert values and Primitives to ASTs or return them + unconverted? """ + block = top_block + + # value blocks don't have a primitive + # (but constant blocks (colors, screen dimensions, etc.) do) + if block.is_value_block(): + raw_value = block.get_value(add_type_prefix=False) + if convert_me: + value_ast = value_to_ast(raw_value) + if value_ast is not None: + return [value_ast] + else: + return [] + else: + if raw_value is not None: + return [raw_value] + else: + return [] + + def _get_prim(block): + prim = lc.get_prim_callable(block.primitive) + # fail gracefully if primitive is not a Primitive object + if not isinstance(prim, Primitive): + raise PyExportError(_("block is not exportable"), block=block) + return prim + + prim = _get_prim(block) + + ast_list = [] + arg_asts = [] + + def _finish_off(block, prim=None): + """ Convert block to an AST and add it to the ast_list. Raise a + PyExportError on failure. """ + if prim is None: + prim = _get_prim(block) + if convert_me: + if prim.export_me: + try: + new_ast = prim.get_ast(*arg_asts) + except ValueError: + traceback.print_exc() + raise PyExportError(_("error while exporting block"), + block=block) + if isinstance(new_ast, (list, tuple)): + ast_list.extend(new_ast) + elif new_ast is not None: + ast_list.append(new_ast) + elif arg_asts: # TODO do we ever get here? + new_ast = ast.List(elts=arg_asts, ctx=ast.Load) + ast_list.append(new_ast) + else: + ast_list.append((prim, ) + tuple(arg_asts)) + + # skip the very first dock/ connection - it's either the previous block or + # the return value of this block + dock_queue = block.docks[1:] + conn_queue = block.connections[1:] + while dock_queue and conn_queue: + dock = dock_queue.pop(0) + conn = conn_queue.pop(0) + if conn is None or dock[0] == 'unavailable': + continue + elif not dock_queue and dock[0] == 'flow': + # finish off this block + _finish_off(block, prim) + arg_asts = [] + # next block + block = conn + prim = _get_prim(block) + dock_queue = block.docks[1:] + conn_queue = block.connections[1:] + else: + # embedded stack of blocks (body of conditional or loop) or + # argument block + if dock[0] == 'flow': + # body of conditional or loop + new_arg_asts = _walk_action_stack(conn, lc, + convert_me=convert_me) + if (prim == LogoCode.prim_loop and + not isinstance(new_arg_asts[-1], ast.Yield)): + new_arg_asts.append(ast_yield_true()) + arg_asts.append(new_arg_asts) + else: + # argument block + new_arg_asts = _walk_action_stack(conn, lc, convert_me=False) + arg_asts.append(*new_arg_asts) + + # finish off last block + _finish_off(block, prim) + + return ast_list + + +def _make_identifier(name): + """ Turn name into a Python identifier name by replacing illegal + characters """ + replaced = re.sub(PAT_IDENTIFIER_ILLEGAL_CHAR, "_", name) + # TODO find better strategy to avoid number at beginning + if re.match("[0-9]", replaced): + replaced = "_" + replaced + return replaced + + +def _indent(code, num_levels=1): + """ Indent each line of code with num_levels * 4 spaces + code -- some python code as a (multi-line) string """ + indentation = " " * (4 * num_levels) + line_list = code.split(linesep) + new_line_list = [] + for line in line_list: + new_line_list.append(indentation + line) + return linesep.join(new_line_list) diff --git a/TurtleArt/tajail.py b/TurtleArt/tajail.py index 40517cd..1a89f1d 100644 --- a/TurtleArt/tajail.py +++ b/TurtleArt/tajail.py @@ -27,24 +27,14 @@ from math import * def myfunc(f, args): ''' Run inline Python code ''' # check to make sure no import calls are made - if len(args) == 1: - myf = 'def f(x): return ' + f.replace('import', '') - userdefined = {} - exec myf in globals(), userdefined - return userdefined.values()[0](args[0]) - elif len(args) == 2: - myf = 'def f(x, y): return ' + f.replace('import', '') - userdefined = {} - exec myf in globals(), userdefined - return userdefined.values()[0](args[0], args[1]) - elif len(args) == 3: - myf = 'def f(x, y, z): return ' + f.replace('import', '') - userdefined = {} - exec myf in globals(), userdefined - return userdefined.values()[0](args[0], args[1], args[2]) - - -def myfunc_import(parent, f, x): + params = ", ".join(['x', 'y', 'z'][:len(args)]) + myf = ''.join(['def f(', params, '): return ', f.replace('import', '')]) + userdefined = {} + exec myf in globals(), userdefined + return userdefined.values()[0](*args) + + +def myfunc_import(parent, f, args): ''' Run Python code imported from Journal ''' if 'def myblock(lc,' in f: base_class = parent.tw.lc # pre-v107, we passed lc @@ -53,7 +43,7 @@ def myfunc_import(parent, f, x): userdefined = {} try: exec f in globals(), userdefined - return userdefined['myblock'](base_class, x) + return userdefined['myblock'](base_class, args) except: traceback.print_exc() return None diff --git a/TurtleArt/talogo.py b/TurtleArt/talogo.py index 0b178c6..ec6e7e7 100644 --- a/TurtleArt/talogo.py +++ b/TurtleArt/talogo.py @@ -22,10 +22,15 @@ #THE SOFTWARE. import gtk +import gobject from time import time, sleep from operator import isNumberType +import os +from os.path import exists as os_path_exists from UserDict import UserDict +import urllib2 +import tempfile try: from sugar.graphics import style @@ -33,10 +38,17 @@ try: except ImportError: GRID_CELL_SIZE = 55 -from taconstants import (TAB_LAYER, DEFAULT_SCALE, PREFIX_DICTIONARY) +import traceback + +from tablock import (Block, Media, media_blocks_dictionary) +from taconstants import (TAB_LAYER, DEFAULT_SCALE, ICON_SIZE) +from tajail import (myfunc, myfunc_import) from tapalette import (block_names, value_blocks) -from tautils import (get_pixbuf_from_journal, convert, data_from_file, - text_media_type, round_int, debug_output, find_group) +from tatype import (TATypeError, TYPES_NUMERIC) +from tautils import (get_pixbuf_from_journal, data_from_file, get_stack_name, + text_media_type, round_int, debug_output, find_group, + get_path, image_to_base64, data_to_string, data_to_file, + get_load_name, chooser_dialog) try: from util.RtfParser import RtfTextOnly @@ -46,7 +58,6 @@ except ImportError: from gettext import gettext as _ -media_blocks_dictionary = {} # new media blocks get added here primitive_dictionary = {} # new block primitives get added here @@ -79,7 +90,21 @@ class logoerror(Exception): return str(self.value) -class HiddenBlock: +class NegativeRootError(BaseException): + """ Similar to the ZeroDivisionError, this error is raised at runtime + when trying to computer the square root of a negative number. """ + + DEFAULT_MESSAGE = 'square root of negative number' + + def __init__(self, neg_value=None, message=DEFAULT_MESSAGE): + self.neg_value = neg_value + self.message = message + + def __str__(self): + return str(self.message) + + +class HiddenBlock(Block): def __init__(self, name, value=None): self.name = name @@ -92,6 +117,7 @@ class HiddenBlock: self.connections = [] self.docks = [] + # Utility functions @@ -142,7 +168,6 @@ class LogoCode: self.hidden_turtle = None - self.keyboard = 0 self.trace = 0 self.update_values = False self.gplay = None @@ -187,6 +212,14 @@ class LogoCode: self.oblist[string] = sym return sym + def get_prim_callable(self, name): + """ Return the callable primitive associated with the given name """ + sym = self.oblist.get(name) + if sym is not None: + return sym.fcn + else: + return None + def run_blocks(self, code): """Run code generated by generate_code(). """ @@ -234,27 +267,17 @@ class LogoCode: blk = action_blk for b in blocks: - if b.name == 'hat1': - code = self._blocks_to_code(b) - self.stacks['stack1'] = self._readline(code) - elif b.name == 'hat2': - code = self._blocks_to_code(b) - self.stacks['stack2'] = self._readline(code) - elif b.name == 'hat': - if b.connections is not None and len(b.connections) > 1 and \ - b.connections[1] is not None: + if b.name in ('hat', 'hat1', 'hat2'): + stack_name = get_stack_name(b) + if stack_name: + stack_key = self._get_stack_key(stack_name) code = self._blocks_to_code(b) - try: - x = b.connections[1].values[0] - except IndexError: - self.tw.showlabel('#nostack') - self.tw.showblocks() - self.tw.running_blocks = False - return None - if isinstance(convert(x, float, False), float): - if int(float(x)) == x: - x = int(x) - self.stacks['stack3' + str(x)] = self._readline(code) + self.stacks[stack_key] = self._readline(code) + else: + self.tw.showlabel('#nostack') + self.tw.showblocks() + self.tw.running_blocks = False + return None code = self._blocks_to_code(blk) @@ -278,7 +301,8 @@ class LogoCode: return ['%nothing%', '%nothing%'] code = [] dock = blk.docks[0] - if len(dock) > 4: # There could be a '(', ')', '[' or ']'. + # There could be a '(', ')', '[' or ']'. + if len(dock) > 4 and dock[4] in ('[', ']', ']['): code.append(dock[4]) if blk.primitive is not None: # make a tuple (prim, blk) if blk in self.tw.block_list.list: @@ -286,37 +310,20 @@ class LogoCode: self.tw.block_list.list.index(blk))) else: code.append(blk.primitive) # Hidden block - elif len(blk.values) > 0: # Extract the value from content blocks. - if blk.name == 'number': - try: - code.append(float(blk.values[0])) - except ValueError: - code.append(float(ord(blk.values[0][0]))) - elif blk.name == 'string' or \ - blk.name == 'title': # deprecated block - if isinstance(blk.values[0], (float, int)): - if int(blk.values[0]) == blk.values[0]: - blk.values[0] = int(blk.values[0]) - code.append('#s' + str(blk.values[0])) - else: - code.append('#s' + blk.values[0]) - elif blk.name in PREFIX_DICTIONARY: - if blk.values[0] is not None: - code.append(PREFIX_DICTIONARY[blk.name] + - str(blk.values[0])) - else: - code.append(PREFIX_DICTIONARY[blk.name] + 'None') - elif blk.name in media_blocks_dictionary: - code.append('#smedia_' + blk.name.upper()) - else: + elif blk.is_value_block(): # Extract the value from content blocks. + value = blk.get_value() + if value is None: return ['%nothing%'] + else: + code.append(value) else: return ['%nothing%'] if blk.connections is not None and len(blk.connections) > 0: for i in range(1, len(blk.connections)): b = blk.connections[i] dock = blk.docks[i] - if len(dock) > 4: # There could be a '(', ')', '[' or ']'. + # There could be a '(', ')', '[' or ']'. + if len(dock) > 4 and dock[4] in ('[', ']', ']['): for c in dock[4]: code.append(c) if b is not None: @@ -346,7 +353,9 @@ class LogoCode: bindex = None if isinstance(token, tuple): (token, bindex) = token - if isNumberType(token): + if isinstance(token, Media): + res.append(token) + elif isNumberType(token): res.append(token) elif token.isdigit(): res.append(float(token)) @@ -401,7 +410,7 @@ class LogoCode: self.istack.append(self.step) self.step = fcn(*(args)) - def evline(self, blklist): + def evline(self, blklist, call_me=True): """ Evaluate a line of code from the list. """ oldiline = self.iline self.iline = blklist[:] @@ -432,7 +441,7 @@ class LogoCode: (token, self.bindex) = self.iline[1] # Process the token and any arguments. - self.icall(self._eval) + self.icall(self._eval, call_me) yield True # Time to unhighlight the current block. @@ -455,7 +464,7 @@ class LogoCode: self.tw.display_coordinates() yield True - def _eval(self): + def _eval(self, call_me=True): """ Evaluate the next token on the line of code we are processing. """ token = self.iline.pop(0) bindex = None @@ -467,7 +476,7 @@ class LogoCode: # We highlight blocks here in case an error occurs... if not self.tw.hide and bindex is not None: self.tw.block_list.list[bindex].highlight() - self.icall(self._evalsym, token) + self.icall(self._evalsym, token, call_me) yield True # and unhighlight if everything was OK. if not self.tw.hide and bindex is not None: @@ -479,7 +488,7 @@ class LogoCode: self.ireturn(res) yield True - def _evalsym(self, token): + def _evalsym(self, token, call_me): """ Process primitive associated with symbol token """ self._undefined_check(token) oldcfun, oldarglist = self.cfun, self.arglist @@ -489,35 +498,53 @@ class LogoCode: self.tw.showblocks() self.tw.display_coordinates() raise logoerror("#noinput") + is_Primitive = type(self.cfun.fcn).__name__ == 'Primitive' + is_PrimitiveDisjunction = type(self.cfun.fcn).__name__ == \ + 'PrimitiveDisjunction' + call_args = not (is_Primitive or is_PrimitiveDisjunction) for i in range(token.nargs): self._no_args_check() - self.icall(self._eval) + self.icall(self._eval, call_args) yield True self.arglist.append(self.iresult) + need_to_pop_istack = False if self.cfun.rprim: if isinstance(self.cfun.fcn, list): # debug_output('evalsym rprim list: %s' % (str(token)), # self.tw.running_sugar) - self.icall(self._ufuncall, self.cfun.fcn) + self.icall(self._ufuncall, self.cfun.fcn, call_args) yield True + need_to_pop_istack = True + result = None else: - self.icall(self.cfun.fcn, *self.arglist) - yield True - result = None + if call_me: + self.icall(self.cfun.fcn, *self.arglist) + yield True + need_to_pop_istack = True + result = None + else: + result = (self.cfun.fcn, ) + tuple(self.arglist) else: - result = self.cfun.fcn(self, *self.arglist) + need_to_pop_istack = True + if call_me: + result = self.cfun.fcn(self, *self.arglist) + else: + result = (self.cfun.fcn, self) + tuple(self.arglist) self.cfun, self.arglist = oldcfun, oldarglist if self.arglist is not None and result is None: self.tw.showblocks() raise logoerror("%s %s %s" % (oldcfun.name, _("did not output to"), self.cfun.name)) - self.ireturn(result) - yield True + if need_to_pop_istack: + self.ireturn(result) + yield True + else: + self.iresult = result - def _ufuncall(self, body): + def _ufuncall(self, body, call_me): """ ufuncall """ - self.ijmp(self.evline, body) + self.ijmp(self.evline, body, call_me) yield True def doevalstep(self): @@ -526,7 +553,9 @@ class LogoCode: try: while (_millisecond() - starttime) < 120: try: - if self.step is not None: + if self.step is None: + return False + if self.tw.running_turtleart: try: self.step.next() except ValueError: @@ -534,23 +563,60 @@ class LogoCode: self.tw.running_sugar) self.tw.running_blocks = False return False + except TATypeError as tte: + # TODO insert the correct block name + # (self.cfun.name is only the name of the + # outermost block in this statement/ line of code) + # use logoerror("#notanumber") when possible + if (tte.req_type in TYPES_NUMERIC and + tte.bad_type not in TYPES_NUMERIC): + raise logoerror("#notanumber") + else: + raise logoerror( + "%s %s %s %s" % + (self.cfun.name, _("doesn't like"), + str(tte.bad_value), _("as input"))) + except ZeroDivisionError: + raise logoerror("#zerodivide") + except NegativeRootError: + raise logoerror("#negroot") + except IndexError: + raise logoerror("#emptyheap") else: - return False + try: + self.step.next() + except BaseException as error: + if isinstance(error, (StopIteration, + logoerror)): + raise error + else: + traceback.print_exc() + self.tw.showlabel( + 'status', '%s: %s' % + (type(error).__name__, str(error))) + return False except StopIteration: - # self.tw.turtles.show_all() - if self.hidden_turtle is not None: - self.hidden_turtle.show() - self.hidden_turtle = None + if self.tw.running_turtleart: + # self.tw.turtles.show_all() + if self.hidden_turtle is not None: + self.hidden_turtle.show() + self.hidden_turtle = None + else: + self.tw.turtles.get_active_turtle().show() + self.tw.running_blocks = False + return False else: - self.tw.turtles.get_active_turtle().show() - self.tw.running_blocks = False - return False + self.ireturn() except logoerror, e: - self.tw.showblocks() - self.tw.display_coordinates() - self.tw.showlabel('syntaxerror', str(e)) - self.tw.turtles.show_all() - self.tw.running_blocks = False + if self.tw.running_turtleart: + self.tw.showblocks() + self.tw.display_coordinates() + self.tw.showlabel('syntaxerror', str(e)) + self.tw.turtles.show_all() + self.tw.running_blocks = False + else: + traceback.print_exc() + self.tw.showlabel('status', 'logoerror: ' + str(e)) return False return True @@ -597,19 +663,282 @@ class LogoCode: name.nargs, name.fcn = 0, body name.rprim = True + def prim_start(self, *ignored_args): + ''' Start block: recenter ''' + if self.tw.running_sugar: + self.tw.activity.recenter() + def prim_clear(self): """ Clear screen """ self.tw.clear_plugins() + self.stop_playing_media() + self.reset_scale() + self.reset_timer() + self.clear_value_blocks() + self.tw.canvas.clearscreen() + self.tw.turtles.reset_turtles() + self.reset_internals() + + def stop_playing_media(self): if self.tw.gst_available: from tagplay import stop_media stop_media(self) - self.tw.canvas.clearscreen() - self.tw.turtles.reset_turtles() + + def reset_scale(self): self.scale = DEFAULT_SCALE - self.hidden_turtle = None + + def reset_timer(self): self.start_time = time() - self.clear_value_blocks() - self.tw.activity.restore_state() + + def get_start_time(self): + return self.start_time + + def reset_internals(self): + self.hidden_turtle = None + if self.tw.running_turtleart: + self.tw.activity.restore_state() + + def prim_loop(self, controller, blklist): + """ Execute a loop + controller -- iterator that yields True iff the loop should be run + once more OR a callable that returns such an iterator + blklist -- list of callables that form the loop body """ + if not hasattr(controller, "next"): + if callable(controller): + controller = controller() + else: + raise TypeError("a loop controller must be either an iterator " + "or a callable that returns an iterator") + while next(controller): + self.icall(self.evline, blklist[:]) + yield True + if self.procstop: + break + self.ireturn() + yield True + + def prim_clamp(self, blklist): + """ Run clamp blklist """ + self.icall(self.evline, blklist[:]) + yield True + self.procstop = False + self.ireturn() + yield True + + def set_scale(self, scale): + ''' Set scale for media blocks ''' + self.scale = scale + + def get_scale(self): + ''' Set scale for media blocks ''' + return self.scale + + def prim_stop_stack(self): + """ Stop execution of a stack """ + self.procstop = True + + def active_turtle(self): + ''' NOP used to add get_active_turtle to Python export ''' + # turtle = self.tw.turtles.get_turtle() + pass + + def prim_turtle(self, name): + self.tw.turtles.set_turtle(name) + + def prim_wait(self, wait_time): + """ Show the turtle while we wait """ + self.tw.turtles.get_active_turtle().show() + endtime = _millisecond() + wait_time * 1000. + while _millisecond() < endtime: + sleep(wait_time / 10.) + yield True + self.tw.turtles.get_active_turtle().hide() + self.ireturn() + yield True + + def prim_if(self, boolean, blklist): + """ If bool, do list """ + if boolean: + self.icall(self.evline, blklist[:]) + yield True + self.ireturn() + yield True + + def prim_ifelse(self, boolean, list1, list2): + """ If bool, do list1, else do list2 """ + if boolean: + self.ijmp(self.evline, list1[:]) + yield True + else: + self.ijmp(self.evline, list2[:]) + yield True + + def prim_set_box(self, name, value): + """ Store value in named box """ + (key, is_native) = self._get_box_key(name) + self.boxes[key] = value + if is_native: + if self.update_values: + self.update_label_value(name, value) + else: + if self.update_values: + self.update_label_value('box', value, label=name) + + def prim_get_box(self, name): + """ Retrieve value from named box """ + (key, is_native) = self._get_box_key(name) + try: + return self.boxes[key] + except KeyError: + # FIXME this looks like a syntax error in the GUI + raise logoerror("#emptybox") + + def _get_box_key(self, name): + """ Return the key used for this box in the boxes dictionary and a + boolean indicating whether it is a 'native' box """ + if name in ('box1', 'box2'): + return (name, True) + else: + # make sure '5' and '5.0' point to the same box + if isinstance(name, (basestring, int, long)): + try: + name = float(name) + except ValueError: + pass + return ('box3_' + str(name), False) + + def prim_define_stack(self, name): + """ Top of a named stack """ + pass + + def prim_invoke_stack(self, name): + """ Process a named stack """ + key = self._get_stack_key(name) + if self.stacks.get(key) is None: + raise logoerror("#nostack") + self.icall(self.evline, self.stacks[key][:]) + yield True + self.procstop = False + self.ireturn() + yield True + + def _get_stack_key(self, name): + """ Return the key used for this stack in the stacks dictionary """ + if name in ('stack1', 'stack2'): + return name + else: + # make sure '5' and '5.0' point to the same action stack + if isinstance(name, (int, long, float)): + if int(name) == name: + name = int(name) + else: + name = float(name) + return 'stack3' + str(name) + + def load_heap(self, obj): + """ Load FILO from file """ + if self.tw.running_sugar: + # Is the object a dsobject? + if isinstance(obj, Media) and obj.value: + from sugar.datastore import datastore + try: + dsobject = datastore.get(obj.value) + except: + debug_output("Couldn't find dsobject %s" % + (obj.value), self.tw.running_sugar) + if dsobject is not None: + self.push_file_data_to_heap(dsobject) + # Or is it a path? + elif os.path.exists(obj): + self.push_file_data_to_heap(None, path=obj) + else: + # Finally try choosing a datastore object + chooser_dialog(self.tw.parent, obj, + self.push_file_data_to_heap) + else: + # If you cannot find the file, open a chooser. + if not os.path.exists(obj): + obj, self.tw.load_save_folder = get_load_name( + '.*', self.tw.load_save_folder) + if obj is not None: + self.push_file_data_to_heap(None, path=obj) + + def save_heap(self, obj): + """ save FILO to file """ + if self.tw.running_sugar: + from sugar import profile + from sugar.datastore import datastore + from sugar.activity import activity + + # Save JSON-encoded heap to temporary file + heap_file = os.path.join(get_path(activity, 'instance'), + 'heap.txt') + data_to_file(self.heap, heap_file) + + # Write to an existing or new dsobject + if isinstance(obj, Media) and obj.value: + dsobject = datastore.get(obj.value) + else: + dsobject = datastore.create() + dsobject.metadata['title'] = str(obj) + dsobject.metadata['icon-color'] = \ + profile.get_color().to_string() + dsobject.metadata['mime_type'] = 'text/plain' + dsobject.set_file_path(heap_file) + datastore.write(dsobject) + dsobject.destroy() + else: + heap_file = obj + data_to_file(self.heap, heap_file) + + def get_heap(self): + return self.heap + + def reset_heap(self): + """ Reset heap to an empty list """ + # empty the list rather than setting it to a new empty list object, + # so the object references are preserved + while self.heap: + self.heap.pop() + + def prim_myblock(self, *args): + """ Run Python code imported from Journal """ + if self.bindex is not None and self.bindex in self.tw.myblock: + try: + myfunc_import(self, self.tw.myblock[self.bindex], args) + except: + raise logoerror("#syntaxerror") + + def prim_myfunction(self, f, *args): + """ Programmable block (Call tajail.myfunc and convert any errors to + logoerrors) """ + try: + y = myfunc(f, args) + if str(y) == 'nan': + debug_output('Python function returned NAN', + self.tw.running_sugar) + self.stop_logo() + raise logoerror("#notanumber") + else: + return y + except ZeroDivisionError: + self.stop_logo() + raise logoerror("#zerodivide") + except ValueError, e: + self.stop_logo() + raise logoerror('#' + str(e)) + except SyntaxError, e: + self.stop_logo() + raise logoerror('#' + str(e)) + except NameError, e: + self.stop_logo() + raise logoerror('#' + str(e)) + except OverflowError: + self.stop_logo() + raise logoerror("#overflowerror") + except TypeError: + self.stop_logo() + raise logoerror("#notanumber") def clear_value_blocks(self): if not hasattr(self, 'value_blocks_to_update'): @@ -686,9 +1015,184 @@ class LogoCode: for blk in drag_group: blk.spr.move_relative((dx, 0)) - def push_file_data_to_heap(self, dsobject): + def reskin(self, obj): + """ Reskin the turtle with an image from a file """ + scale = int(ICON_SIZE * float(self.scale) / DEFAULT_SCALE) + if scale < 1: + return + self.filepath = None + self.dsobject = None + + if obj.value is not None and os_path_exists(obj.value): + self.filepath = obj.value + elif self.tw.running_sugar: # datastore object + from sugar.datastore import datastore + try: + self.dsobject = datastore.get(obj.value) + except: + debug_output("Couldn't find dsobject %s" % + (obj.value), self.tw.running_sugar) + if self.dsobject is not None: + self.filepath = self.dsobject.file_path + + if self.filepath is None: + self.tw.showlabel('nojournal', self.filepath) + return + + pixbuf = None + try: + pixbuf = gtk.gdk.pixbuf_new_from_file_at_size( + self.filepath, scale, scale) + except: + self.tw.showlabel('nojournal', self.filepath) + debug_output("Couldn't open skin %s" % (self.filepath), + self.tw.running_sugar) + if pixbuf is not None: + self.tw.turtles.get_active_turtle().set_shapes([pixbuf]) + pen_state = self.tw.turtles.get_active_turtle().get_pen_state() + if pen_state: + self.tw.turtles.get_active_turtle().set_pen_state(False) + self.tw.turtles.get_active_turtle().forward(0) + if pen_state: + self.tw.turtles.get_active_turtle().set_pen_state(True) + + if self.tw.sharing(): + if self.tw.running_sugar: + tmp_path = get_path(self.tw.activity, 'instance') + else: + tmp_path = '/tmp' + tmp_file = os.path.join(get_path(self.tw.activity, 'instance'), + 'tmpfile.png') + pixbuf.save(tmp_file, 'png', {'quality': '100'}) + data = image_to_base64(tmp_file, tmp_path) + height = pixbuf.get_height() + width = pixbuf.get_width() + event = 'R|%s' % (data_to_string([self.tw.nick, + [round_int(width), + round_int(height), + data]])) + gobject.idle_add(self.tw.send_event, event) + os.remove(tmp_file) + + def get_from_url(self, url): + """ Get contents of URL as text or tempfile to image """ + if "://" not in url: # no protocol + url = "http://" + url # assume HTTP + try: + req = urllib2.urlopen(url) + except urllib2.HTTPError, e: + debug_output("Couldn't open %s: %s" % (url, e), + self.tw.running_sugar) + raise logoerror(url + ' [%d]' % (e.code)) + except urllib2.URLError, e: + if hasattr(e, 'code'): + debug_output("Couldn't open %s: %s" % (url, e), + self.tw.running_sugar) + raise logoerror(url + ' [%d]' % (e.code)) + else: # elif hasattr(e, 'reason'): + debug_output("Couldn't reach server: %s" % (e), + self.tw.running_sugar) + raise logoerror('#noconnection') + + if req.info().getheader("Content-Type")[0:5] == "image": + # it can't be deleted immediately, or else we won't ever access it + tmp = tempfile.NamedTemporaryFile(delete=False) + tmp.write(req.read()) # prepare for writing + tmp.flush() # actually write it + obj = Media('media', value=tmp.name) + return obj + else: + return req.read() + + def showlist(self, objects): + """ Display list of media objects """ + x = (self.tw.turtles.get_active_turtle().get_xy()[0] / + self.tw.coord_scale) + y = (self.tw.turtles.get_active_turtle().get_xy()[1] / + self.tw.coord_scale) + for obj in objects: + self.tw.turtles.get_active_turtle().set_xy(x, y, pendown=False) + self.show(obj) + y -= int(self.tw.canvas.textsize * self.tw.lead) + + def show(self, obj, center=False): + """ Show is the general-purpose media-rendering block. """ + # media + if isinstance(obj, Media) and obj.value: + self.filepath = None + self.pixbuf = None # Camera writes directly to pixbuf + self.dsobject = None + + # camera snapshot + if obj.value.lower() in media_blocks_dictionary: + media_blocks_dictionary[obj.value.lower()]() + # file path + elif os_path_exists(obj.value): + self.filepath = obj.value + # datastore object + elif self.tw.running_sugar: + from sugar.datastore import datastore + try: + self.dsobject = datastore.get(obj.value) + except: + debug_output("Couldn't find dsobject %s" % + (obj.value), self.tw.running_sugar) + if self.dsobject is not None: + self.filepath = self.dsobject.file_path + + if self.pixbuf is not None: + self.insert_image(center=center, pixbuf=True) + elif self.filepath is None: + if self.dsobject is not None: + self.tw.showlabel( + 'nojournal', + self.dsobject.metadata['title']) + else: + self.tw.showlabel('nojournal', obj.value) + debug_output("Couldn't open %s" % (obj.value), + self.tw.running_sugar) + elif obj.type == 'media': + self.insert_image(center=center) + elif obj.type == 'descr': + mimetype = None + if self.dsobject is not None and \ + 'mime_type' in self.dsobject.metadata: + mimetype = self.dsobject.metadata['mime_type'] + description = None + if self.dsobject is not None and \ + 'description' in self.dsobject.metadata: + description = self.dsobject.metadata[ + 'description'] + self.insert_desc(mimetype, description) + elif obj.type == 'audio': + self.play_sound() + elif obj.type == 'video': + self.play_video() + + if self.dsobject is not None: + self.dsobject.destroy() + + # text or number + elif isinstance(obj, (basestring, float, int)): + if isinstance(obj, (float, int)): + obj = round_int(obj) + x, y = self.x2tx(), self.y2ty() + if center: + y -= self.tw.canvas.textsize + self.tw.turtles.get_active_turtle().draw_text( + obj, x, y, + int(self.tw.canvas.textsize * self.scale / 100.), + self.tw.canvas.width - x) + + def push_file_data_to_heap(self, dsobject, path=None): """ push contents of a data store object (assuming json encoding) """ - data = data_from_file(dsobject.file_path) + if dsobject: + data = data_from_file(dsobject.file_path) + elif path is not None: + data = data_from_file(path) + else: + data = None + debug_output("No file to open", self.tw.running_sugar) if data is not None: for val in data: self.heap.append(val) @@ -861,7 +1365,7 @@ class LogoCode: def _expand_forever(self, b, blk, blocks): """ Expand a while or until block into: forever, ifelse, stopstack - Expand a forever block to run in a separate stack + Expand a forever block to run in a separate stack Parameters: the loop block, the top block, all blocks. Return the start block of the expanded loop, and all blocks.""" @@ -911,7 +1415,7 @@ class LogoCode: first_label_blk = HiddenBlock('string', value=action_flow_name) # Assign new connections and build the docks - if inflow is not None: + if inflow is not None and b in inflow.connections: i = inflow.connections.index(b) if until_blk and whileflow is not None: inflow.connections[i] = action_first @@ -980,14 +1484,14 @@ class LogoCode: # Create a separate stacks for the forever loop and the whileflow code = self._blocks_to_code(forever_blk) - self.stacks['stack3' + str(action_name)] = self._readline(code) + self.stacks[self._get_stack_key(action_name)] = self._readline(code) if until_blk and whileflow is not None: # Create a stack from the whileflow to be called from # action_first, but then reconnect it to the ifelse block c = whileflow.connections[0] whileflow.connections[0] = None code = self._blocks_to_code(whileflow) - self.stacks['stack3' + str(action_flow_name)] = \ + self.stacks[self._get_stack_key(action_flow_name)] = \ self._readline(code) whileflow.connections[0] = c diff --git a/TurtleArt/tapalette.py b/TurtleArt/tapalette.py index 6fd347a..2cf568a 100644 --- a/TurtleArt/tapalette.py +++ b/TurtleArt/tapalette.py @@ -71,6 +71,7 @@ block_styles = {'basic-style': [], 'clamp-style-collapsed': [], 'clamp-style-1arg': [], 'clamp-style-boolean': [], + 'clamp-style-until': [], 'clamp-style-else': [], 'portfolio-style-2x2': [], 'portfolio-style-1x1': [], @@ -90,7 +91,6 @@ except ImportError: HELP_PALETTE = False from taconstants import (EXPANDABLE_STYLE, EXPANDABLE_FLOW) -from tautils import debug_output from gettext import gettext as _ @@ -177,7 +177,7 @@ class Palette(): logo_command=None, hidden=False, colors=None, string_or_number=False): """ Add a new block to the palette """ - block = Block(block_name) + block = _ProtoBlock(block_name) block.set_style(style) if label is not None: block.set_label(label) @@ -224,7 +224,7 @@ class Palette(): def make_palette(palette_name, colors=None, help_string=None, position=None, - init_on_start=False): + init_on_start=False, translation=None): """ Palette helper function """ if colors is None: palette = Palette(palette_name) @@ -252,7 +252,7 @@ def define_logo_function(key, value): logo_functions[key] = value -class Block(): +class _ProtoBlock(): """ a class for defining new block primitives """ def __init__(self, name): @@ -290,6 +290,7 @@ class Block(): 'clamp-style-collapsible', 'clamp-style-1arg', 'clamp-style-boolean', + 'clamp-style-until', 'clamp-style-else']: EXPANDABLE_FLOW.append(self._name) diff --git a/TurtleArt/taplugin.py b/TurtleArt/taplugin.py new file mode 100644 index 0000000..4cf2563 --- /dev/null +++ b/TurtleArt/taplugin.py @@ -0,0 +1,117 @@ +#Copyright (c) 2013 Walter Bender +#Copyright (c) 2013 Daniel Francis + +#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 ConfigParser +from gettext import gettext as _ +import os +import shutil +import subprocess +from TurtleArt.tapalette import (palette_names, help_strings) + + +def cancel_plugin_install(self, tmp_dir): + ''' If we cancel, just cleanup ''' + shutil.rmtree(tmp_dir) + + +def complete_plugin_install(self, tmp_dir, tmp_path, plugin_path, + plugin_name, file_info): + ''' We complete the installation directly or from ConfirmationAlert ''' + status = subprocess.call(['cp', '-r', tmp_path, plugin_path + '/']) + if status == 0: + # Save the plugin.info file in the plugin directory + subprocess.call(['cp', os.path.join(tmp_dir, 'plugin.info'), + os.path.join(plugin_path, plugin_name) + '/']) + if self.has_toolbarbox: + palette_name_list = [] + if file_info.has_option('Plugin', 'palette'): + palette_name_list = file_info.get( + 'Plugin', 'palette').split(',') + create_palette = [] + for palette_name in palette_name_list: + if not palette_name.strip() in palette_names: + create_palette.append(True) + else: + create_palette.append(False) + self.tw.init_plugin(plugin_name) + self.tw.turtleart_plugins[-1].setup() + self.tw.load_media_shapes() + for i, palette_name in enumerate(palette_name_list): + if create_palette[i]: + j = len(self.palette_buttons) + self.palette_buttons.append( + self._radio_button_factory( + palette_name.strip() + 'off', + self._palette_toolbar, + self.do_palette_buttons_cb, + j - 1, + help_strings[palette_name.strip()], + self.palette_buttons[0])) + self._overflow_buttons.append( + self._add_button( + palette_name.strip() + 'off', + None, + self.do_palette_buttons_cb, + None, + arg=j - 1)) + self._overflow_box.pack_start( + self._overflow_buttons[j - 1]) + self.tw.palettes.insert(j - 1, []) + self.tw.palette_sprs.insert(j - 1, [None, None]) + else: + # We need to change the index associated with the + # Trash Palette Button. + j = len(palette_names) + pidx = palette_names.index(palette_name.strip()) + self.palette_buttons[pidx].connect( + 'clicked', self.do_palette_buttons_cb, j - 1) + self._overflow_buttons[pidx].connect( + 'clicked', self.do_palette_buttons_cb, j - 1) + self._setup_palette_toolbar() + else: + self.tw.showlabel('status', label=_('Please restart Turtle Art \ +in order to use the plugin.')) + else: + self.tw.showlabel('status', label=_('Plugin could not be installed.')) + status = subprocess.call(['rm', '-r', tmp_path]) + shutil.rmtree(tmp_dir) + + +def load_a_plugin(self, tmp_dir): + ''' Load a plugin from the Journal and initialize it ''' + plugin_path = os.path.join(tmp_dir, 'plugin.info') + file_info = ConfigParser.ConfigParser() + if len(file_info.read(plugin_path)) == 0: + self.tw.showlabel('status', + label=_('Plugin could not be installed.')) + elif not file_info.has_option('Plugin', 'name'): + self.tw.showlabel( + 'status', label=_('Plugin could not be installed.')) + else: + plugin_name = file_info.get('Plugin', 'name') + tmp_path = os.path.join(tmp_dir, plugin_name) + plugin_path = os.path.join(self.bundle_path, 'plugins') + if os.path.exists(os.path.join(plugin_path, plugin_name)): + self._reload_plugin_alert(tmp_dir, tmp_path, plugin_path, + plugin_name, file_info) + else: + complete_plugin_install(self, tmp_dir, tmp_path, plugin_path, + plugin_name, file_info) diff --git a/TurtleArt/taprimitive.py b/TurtleArt/taprimitive.py new file mode 100644 index 0000000..197a8d8 --- /dev/null +++ b/TurtleArt/taprimitive.py @@ -0,0 +1,1174 @@ +#Copyright (c) 2013 Marion Zepf + +#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 ast +from gettext import gettext as _ +from math import sqrt +from random import uniform +import traceback + +#from ast_pprint import * # only used for debugging, safe to comment out + +from tablock import Media +from tacanvas import TurtleGraphics +from taconstants import (Color, CONSTANTS) +from talogo import (LogoCode, logoerror, NegativeRootError) +from taturtle import (Turtle, Turtles) +from TurtleArt.tatype import (TYPE_CHAR, TYPE_INT, TYPE_FLOAT, TYPE_OBJECT, + TYPE_MEDIA, TYPE_COLOR, BOX_AST, ACTION_AST, + Type, TypeDisjunction, TATypeError, get_type, + TypedSubscript, TypedName, is_bound_method, + is_instancemethod, is_staticmethod, + identity, get_converter, convert, get_call_ast) +from tautils import debug_output +from tawindow import (TurtleArtWindow, global_objects, plugins_in_use) +from util import ast_extensions + + +class PyExportError(BaseException): + """ Error that is raised when something goes wrong while converting the + blocks to python code """ + + def __init__(self, message, block=None): + """ message -- the error message + block -- the block where the error occurred """ + self.message = message + self.block = block + + def __str__(self): + if self.block is not None: + return _("error in highlighted block") + ": " + str(self.message) + else: + return _("error") + ": " + str(self.message) + + +class Primitive(object): + """ Something that can be called when the block code is executed in TA, + but that can also be transformed into a Python AST.""" + + _DEBUG = False + + STANDARD_OPERATORS = {'plus': (ast.UAdd, ast.Add), + 'minus': (ast.USub, ast.Sub), + 'multiply': ast.Mult, + 'divide': ast.Div, + 'modulo': ast.Mod, + 'power': ast.Pow, + 'and_': ast.And, + 'or_': ast.Or, + 'not_': ast.Not, + 'equals': ast.Eq, + 'less': ast.Lt, + 'greater': ast.Gt} + + def __init__(self, func, return_type=TYPE_OBJECT, arg_descs=None, + kwarg_descs=None, call_afterwards=None, export_me=True): + """ return_type -- the type (from the type hierarchy) that this + Primitive will return + arg_descs, kwarg_descs -- a list of argument descriptions and + a dictionary of keyword argument descriptions. An argument + description can be either an ArgSlot or a ConstantArg. + call_afterwards -- Code to call after this Primitive has been called + (e.g., for updating labels in LogoCode) (not used for creating + AST) + export_me -- True iff this Primitive should be exported to Python + code (the default case) """ + self.func = func + self.return_type = return_type + + if arg_descs is None: + self.arg_descs = [] + else: + self.arg_descs = arg_descs + + if kwarg_descs is None: + self.kwarg_descs = {} + else: + self.kwarg_descs = kwarg_descs + + self.call_afterwards = call_afterwards + self.export_me = export_me + + def copy(self): + """ Return a Primitive object with the same attributes as this one. + Shallow-copy the arg_descs and kwarg_descs attributes. """ + arg_descs_copy = self.arg_descs[:] + if isinstance(self.arg_descs, ArgListDisjunction): + arg_descs_copy = ArgListDisjunction(arg_descs_copy) + return Primitive(self.func, + return_type=self.return_type, + arg_descs=arg_descs_copy, + kwarg_descs=self.kwarg_descs.copy(), + call_afterwards=self.call_afterwards, + export_me=self.export_me) + + def __repr__(self): + return "Primitive(%s -> %s)" % (repr(self.func), str(self.return_type)) + + @property + def __name__(self): + return self.func.__name__ + + def get_name_for_export(self): + """ Return the expression (as a string) that represents this Primitive + in the exported Python code, e.g., 'turtle.forward'. """ + func_name = "" + if self.wants_turtle(): + func_name = "turtle." + elif self.wants_turtles(): + func_name = "turtles." + elif self.wants_canvas(): + func_name = "canvas." + elif self.wants_logocode(): + func_name = "logo." + elif self.wants_heap(): + func_name = "logo.heap." + elif self.wants_tawindow(): + func_name = "tw." + else: + results, plugin = self.wants_plugin() + if results: + for k in global_objects.keys(): + if k == plugin: + if k not in plugins_in_use: + plugins_in_use.append(k) + func_name = k.lower() + '.' + break + + # get the name of the function directly from the function itself + func_name += self.func.__name__ + return func_name + + def are_slots_filled(self): + """ Return True iff none of the arg_descs or kwarg_descs is an + ArgSlot. """ + for arg_desc in self.arg_descs: + if isinstance(arg_desc, ArgSlot): + return False + for key in self.kwarg_descs: + if isinstance(self.kwarg_descs[key], ArgSlot): + return False + return True + + def fill_slots(self, arguments=None, keywords=None, convert_to_ast=False, + call_my_args=True): + """ Return a copy of this Primitive whose ArgSlots are filled with + the given arguments, turned into ConstantArgs. Call the arguments, + apply their wrappers, and check their types as appropriate. """ + if arguments is None: + arguments = [] + if keywords is None: + keywords = {} + + new_prim = self.copy() + + if isinstance(new_prim.arg_descs, ArgListDisjunction): + slot_list_alternatives = list(new_prim.arg_descs) + else: + slot_list_alternatives = [new_prim.arg_descs] + + # arguments + error = None + filler = None + for slot_list in slot_list_alternatives: + error = None + new_slot_list = [] + filler_list = list(arguments[:]) + for slot in slot_list: + if isinstance(slot, ArgSlot): + filler = filler_list.pop(0) + try: + const = slot.fill(filler, + convert_to_ast=convert_to_ast, + call_my_args=call_my_args) + except TATypeError as error: + if Primitive._DEBUG: + traceback.print_exc() + break + else: + new_slot_list.append(const) + else: + new_slot_list.append(slot) + if error is None: + new_prim.arg_descs = new_slot_list + break + if error is not None: + raise error + + # keyword arguments + for key in keywords: + kwarg_desc = new_prim.kwarg_descs[key] + if isinstance(kwarg_desc, ArgSlot): + const = kwarg_desc.fill(keywords[key], + convert_to_ast=convert_to_ast, + call_my_args=call_my_args) + new_prim.kwarg_descs[key] = const + + return new_prim + + def get_values_of_filled_slots(self, exportable_only=False): + """ Return the values of all filled argument slots as a list, and + the values of all filled keyword argument slots as a dictionary. + Ignore all un-filled (keyword) argument slots. + exportable_only -- return only exportable values and convert values + to ASTs instead of calling them """ + new_args = [] + for c_arg in self.arg_descs: + if (isinstance(c_arg, ConstantArg) + and (not exportable_only + or export_me(c_arg.value))): + new_args.append(c_arg.get(convert_to_ast=exportable_only)) + new_kwargs = {} + for key in self.kwarg_descs: + if (isinstance(self.kwarg_descs[key], ConstantArg) + and (not exportable_only + or export_me(self.kwarg_descs[key].value))): + new_kwargs[key] = self.kwarg_descs[key].get( + convert_to_ast=exportable_only) + return (new_args, new_kwargs) + + def allow_call_args(self, recursive=False): + """ Set call_args attribute of all argument descriptions to True + recursive -- recursively call allow_call_args on all constant args + that are Primitives """ + for arg_desc in self.arg_descs: + arg_desc.call_arg = True + if (recursive and isinstance(arg_desc, ConstantArg) and + isinstance(arg_desc.value, Primitive)): + arg_desc.value.allow_call_args(recursive=True) + for kwarg_desc in self.kwarg_descs: + kwarg_desc.call_arg = True + if (recursive and isinstance(kwarg_desc, ConstantArg) and + isinstance(kwarg_desc.value, Primitive)): + kwarg_desc.value.allow_call_args(recursive=True) + + def __call__(self, *runtime_args, **runtime_kwargs): + """ Execute the function, passing it the arguments received at + runtime. Also call the function in self.call_afterwards and pass it + all runtime_args and runtime_kwargs. + If the very first argument is a LogoCode instance, it is removed. + The active turtle, the Turtles object, the canvas, the LogoCode + object, or the TurtleArtWindow object will be prepended to the + arguments (depending on what this Primitive wants). """ + + # remove the first argument if it is a LogoCode instance + if runtime_args and isinstance(runtime_args[0], LogoCode): + runtime_args = runtime_args[1:] + + if Primitive._DEBUG: + debug_output(repr(self)) + debug_output(" runtime_args: " + repr(runtime_args)) + # fill the ArgSlots with the runtime arguments + new_prim = self.fill_slots(runtime_args, runtime_kwargs, + convert_to_ast=False) + if not new_prim.are_slots_filled(): + raise logoerror("#syntaxerror") + if Primitive._DEBUG: + debug_output(" new_prim.arg_descs: " + repr(new_prim.arg_descs)) + + # extract the actual values from the (now constant) arguments + (new_args, new_kwargs) = new_prim.get_values_of_filled_slots() + if Primitive._DEBUG: + debug_output(" new_args: " + repr(new_args)) + debug_output("end " + repr(self)) + + # what does this primitive want as its first argument? + first_arg = None + if not is_bound_method(new_prim.func): + if new_prim.wants_turtle(): + first_arg = global_objects["turtles"].get_active_turtle() + elif new_prim.wants_turtles(): + first_arg = global_objects["turtles"] + elif new_prim.wants_canvas(): + first_arg = global_objects["canvas"] + elif new_prim.wants_logocode(): + first_arg = global_objects["logo"] + elif new_prim.wants_heap(): + first_arg = global_objects["logo"].heap + elif new_prim.wants_tawindow(): + first_arg = global_objects["window"] + else: + result, plugin = new_prim.wants_plugin() + if result: + first_arg = plugin + + # execute the actual function + if first_arg is None: + return_value = new_prim.func(*new_args, **new_kwargs) + else: + return_value = new_prim.func(first_arg, *new_args, **new_kwargs) + + if new_prim.call_afterwards is not None: + new_prim.call_afterwards(*new_args, **new_kwargs) + + return return_value + + def get_ast(self, *arg_asts, **kwarg_asts): + """Transform this object into a Python AST. When serialized and + executed, the AST will do exactly the same as calling this + object.""" + + if Primitive._DEBUG: + debug_output(repr(self)) + debug_output(" arg_asts: " + repr(arg_asts)) + new_prim = self.fill_slots(arg_asts, kwarg_asts, convert_to_ast=True) + if not new_prim.are_slots_filled(): + raise PyExportError("not enough arguments") + if Primitive._DEBUG: + debug_output(" new_prim.arg_descs: " + repr(new_prim.arg_descs)) + + # extract the actual values from the (now constant) arguments + (new_arg_asts, new_kwarg_asts) = new_prim.get_values_of_filled_slots( + exportable_only=True) + if Primitive._DEBUG: + debug_output(" new_arg_asts: " + repr(new_arg_asts)) + debug_output("end " + repr(self)) + + # SPECIAL HANDLING # + + # loops + if self == LogoCode.prim_loop: + controller = self._get_loop_controller() + if controller == Primitive.controller_repeat: + # 'repeat' loop + num_repetitions = new_arg_asts[0] + if num_repetitions.func.id == 'controller_repeat': + num_repetitions = num_repetitions.args[0] + repeat_iter = get_call_ast("range", [num_repetitions]) + # TODO use new variable name in nested loops + loop_ast = ast.For(target=ast.Name(id="i", ctx=ast.Store), + iter=repeat_iter, + body=new_arg_asts[1], + orelse=[]) + return loop_ast + else: + if controller == Primitive.controller_forever: + condition_ast = ast.Name(id="True", ctx=ast.Load) + elif controller == Primitive.controller_while: + condition_ast = new_arg_asts[0].args[0] + elif controller == Primitive.controller_until: + pos_cond_ast = new_arg_asts[0].args[0] + condition_ast = ast.UnaryOp(op=ast.Not, + operand=pos_cond_ast) + else: + raise PyExportError("unknown loop controller: " + + repr(controller)) + loop_ast = ast.While(test=condition_ast, + body=new_arg_asts[1], + orelse=[]) + # Until always executes its body once. + if controller == Primitive.controller_until: + loop_list = [] + for arg_ast in new_arg_asts[1]: + loop_list.append(arg_ast) + loop_list.append(loop_ast) + return loop_list + else: + return loop_ast + + # conditionals + elif self in (LogoCode.prim_if, LogoCode.prim_ifelse): + test = new_arg_asts[0] + body = new_arg_asts[1] + if len(new_arg_asts) > 2: + orelse = new_arg_asts[2] + else: + orelse = [] + if_ast = ast.If(test=test, body=body, orelse=orelse) + return if_ast + + # boxes + elif self == LogoCode.prim_set_box: + target_ast = ast.Subscript(value=BOX_AST, + slice=ast.Index(value=new_arg_asts[0]), + ctx=ast.Store) + return ast.Assign(targets=[target_ast], value=new_arg_asts[1]) + elif self == LogoCode.prim_get_box: + return ast.Subscript(value=BOX_AST, + slice=ast.Index(value=new_arg_asts[0]), + ctx=ast.Load) + + # action stacks + elif self == LogoCode.prim_define_stack: + return + elif self == LogoCode.prim_invoke_stack: + stack_func = ast.Subscript( + value=ACTION_AST, + slice=ast.Index(value=new_arg_asts[0]), ctx=ast.Load) + call_ast = get_call_ast('logo.icall', [stack_func]) + return [call_ast, ast_yield_true()] + + # stop stack + elif self == LogoCode.prim_stop_stack: + return ast.Return() + + # sleep/ wait + elif self == LogoCode.prim_wait: + return [get_call_ast('sleep', new_arg_asts), ast_yield_true()] + + # standard operators + elif self.func.__name__ in Primitive.STANDARD_OPERATORS: + op = Primitive.STANDARD_OPERATORS[self.func.__name__] + # 'divide': prevent unwanted integer division + if self == Primitive.divide: + def _is_float(x): + return get_type(x)[0] == TYPE_FLOAT + if (not _is_float(new_arg_asts[0]) and + not _is_float(new_arg_asts[1])): + new_arg_asts[0] = get_call_ast('float', [new_arg_asts[0]], + return_type=TYPE_FLOAT) + if len(new_arg_asts) == 1: + if isinstance(op, tuple): + op = op[0] + return ast.UnaryOp(op=op, operand=new_arg_asts[0]) + elif len(new_arg_asts) == 2: + if isinstance(op, tuple): + op = op[1] + (left, right) = new_arg_asts + if issubclass(op, ast.boolop): + return ast.BoolOp(op=op, values=[left, right]) + elif issubclass(op, ast.cmpop): + return ast.Compare(left=left, ops=[op], + comparators=[right]) + else: + return ast.BinOp(op=op, left=left, right=right) + + # f(x) + elif self == LogoCode.prim_myfunction: + param_asts = [] + for id_ in ['x', 'y', 'z'][:len(new_arg_asts)-1]: + param_asts.append(ast.Name(id=id_, ctx=ast.Param)) + func_ast = ast_extensions.LambdaWithStrBody( + body_str=new_arg_asts[0].s, args=param_asts) + return get_call_ast(func_ast, new_arg_asts[1:], + return_type=self.return_type) + + # square root + elif self == Primitive.square_root: + return get_call_ast('sqrt', new_arg_asts, new_kwarg_asts, + return_type=self.return_type) + + # random + elif self in (Primitive.random_char, Primitive.random_int): + uniform_ast = get_call_ast('uniform', new_arg_asts) + round_ast = get_call_ast('round', [uniform_ast, ast.Num(n=0)]) + int_ast = get_call_ast('int', [round_ast], return_type=TYPE_INT) + if self == Primitive.random_char: + chr_ast = get_call_ast('chr', [int_ast], return_type=TYPE_CHAR) + return chr_ast + else: + return int_ast + + # identity + elif self == Primitive.identity: + return new_arg_asts[0] + + # constant + elif self == CONSTANTS.get: + return TypedSubscript(value=ast.Name(id='CONSTANTS', ctx=ast.Load), + slice_=ast.Index(value=new_arg_asts[0]), + return_type=self.return_type) + + # group of Primitives or sandwich-clamp block + elif self in (Primitive.group, LogoCode.prim_clamp): + ast_list = [] + for prim in new_arg_asts[0]: + if export_me(prim): + new_ast = value_to_ast(prim) + if isinstance(new_ast, ast.AST): + ast_list.append(new_ast) + return ast_list + + # set turtle + elif self == LogoCode.prim_turtle: + text = 'turtle = turtles.get_active_turtle()' + return [get_call_ast('logo.prim_turtle', new_arg_asts), + ast_extensions.ExtraCode(text)] + + elif self == LogoCode.active_turtle: + text = 'turtle = turtles.get_active_turtle()' + return ast_extensions.ExtraCode(text) + + # comment + elif self == Primitive.comment: + if isinstance(new_arg_asts[0], ast.Str): + text = ' ' + str(new_arg_asts[0].s) + else: + text = ' ' + str(new_arg_asts[0]) + return ast_extensions.Comment(text) + + # print + elif self == TurtleArtWindow.print_: + func_name = self.get_name_for_export() + call_ast = get_call_ast(func_name, new_arg_asts) + print_ast = ast.Print(values=new_arg_asts[:1], dest=None, nl=True) + return [call_ast, print_ast] + + # heap + elif self == LogoCode.get_heap: + return TypedName(id_='logo.heap', return_type=self.return_type) + elif self == LogoCode.reset_heap: + target_ast = ast.Name(id='logo.heap', ctx=ast.Store) + value_ast = ast.List(elts=[], ctx=ast.Load) + return ast.Assign(targets=[target_ast], value=value_ast) + + # NORMAL FUNCTION CALL # + + else: + func_name = self.get_name_for_export() + return get_call_ast(func_name, new_arg_asts, new_kwarg_asts, + return_type=self.return_type) + + def __eq__(self, other): + """ Two Primitives are equal iff their all their properties are equal. + Consider bound and unbound methods equal. """ + # other is a Primitive + if isinstance(other, Primitive): + return (self == other.func and + self.return_type == other.return_type and + self.arg_descs == other.arg_descs and + self.kwarg_descs == other.kwarg_descs and + self.call_afterwards == other.call_afterwards and + self.export_me == other.export_me) + + # other is a callable + elif callable(other): + if is_instancemethod(self.func) != is_instancemethod(other): + return False + elif is_instancemethod(self.func): # and is_instancemethod(other): + return (self.func.im_class == other.im_class and + self.func.im_func == other.im_func) + else: + return self.func == other + + elif is_staticmethod(other): + return self.func == other.__func__ + + # other is neither a Primitive nor a callable + else: + return False + + def wants_turtle(self): + """Does this Primitive want to get the active turtle as its first + argument?""" + return self._wants(Turtle) + + def wants_turtles(self): + """ Does this Primitive want to get the Turtles instance as its + first argument? """ + return self._wants(Turtles) + + def wants_canvas(self): + """ Does this Primitive want to get the canvas as its first + argument? """ + return self._wants(TurtleGraphics) + + def wants_logocode(self): + """ Does this Primitive want to get the LogoCode instance as its + first argument? """ + return (self.func.__name__ == '' or self._wants(LogoCode)) + + def wants_heap(self): + """ Does this Primitive want to get the heap as its first argument? """ + return ((hasattr(self.func, '__self__') and + isinstance(self.func.__self__, list)) or + self.func in list.__dict__.values()) + + def wants_tawindow(self): + """ Does this Primitive want to get the TurtleArtWindow instance + as its first argument? """ + return self._wants(TurtleArtWindow) + + def wants_plugin(self): + """Does this Primitive want to get a plugin instance as its first + argument? """ + for obj in global_objects.keys(): + if self._wants(global_objects[obj].__class__): + return True, obj + return False, None + + def wants_nothing(self): + """Does this Primitive want nothing as its first argument? I.e. does + it want to be passed all the arguments of the block and + nothing else?""" + return not is_instancemethod(self.func) + + def _wants(self, theClass): + return is_instancemethod(self.func) and self.func.im_class == theClass + + # treat the following methods in a special way when converting the + # Primitive to an AST + + @staticmethod + def controller_repeat(num): + """ Loop controller for the 'repeat' block """ + for i in range(num): + yield True + yield False + + @staticmethod + def controller_forever(): + """ Loop controller for the 'forever' block """ + while True: + yield True + + @staticmethod + def controller_while(condition): + """ Loop controller for the 'while' block + condition -- Primitive that is evaluated every time through the + loop """ + condition.allow_call_args(recursive=True) + while condition(): + yield True + yield False + + @staticmethod + def controller_until(condition): + """ Loop controller for the 'until' block + condition -- Primitive that is evaluated every time through the + loop """ + condition.allow_call_args(recursive=True) + while not condition(): + yield True + yield False + + LOOP_CONTROLLERS = [controller_repeat, controller_forever, + controller_while, controller_until] + + def _get_loop_controller(self): + """ Return the controller for this loop Primitive. Raise a + ValueError if no controller was found. """ + def _is_loop_controller(candidate): + return (callable(candidate) + and candidate in Primitive.LOOP_CONTROLLERS) + + for desc in self.arg_descs: + if isinstance(desc, ConstantArg): + value = desc.value + if _is_loop_controller(value): + return value + elif isinstance(desc, ArgSlot): + wrapper = desc.wrapper + if _is_loop_controller(wrapper): + return wrapper + + # no controller found + raise PyExportError("found no loop controller for " + repr(self)) + + @staticmethod + def do_nothing(): + pass + + @staticmethod + def identity(arg): + """ Return the argument unchanged """ + return arg + + @staticmethod + def group(prim_list): + """ Group together multiple Primitives into one. Treat each Primitive + as a separate line of code. """ + return_val = None + for prim in prim_list: + return_val = prim() + return return_val + + @staticmethod + def plus(arg1, arg2=None): + """ If only one argument is given, prefix it with '+'. If two + arguments are given, add the second to the first. If the first + argument is a tuple of length 2 and the second is None, use the + values in the tuple as arg1 and arg2. """ + if isinstance(arg1, (list, tuple)) and len(arg1) == 2 and arg2 is None: + (arg1, arg2) = arg1 + if arg2 is None: + return + arg1 + else: + return arg1 + arg2 + + @staticmethod + def minus(arg1, arg2=None): + """ If only one argument is given, change its sign. If two + arguments are given, subtract the second from the first. """ + if arg2 is None: + return - arg1 + else: + return arg1 - arg2 + + @staticmethod + def multiply(arg1, arg2): + """ Multiply the two arguments """ + return arg1 * arg2 + + @staticmethod + def divide(arg1, arg2): + """ Divide the first argument by the second """ + return float(arg1) / arg2 + + @staticmethod + def modulo(arg1, arg2): + """ Return the remainder of dividing the first argument by the second. + If the first argument is a string, format it with the value(s) in + the second argument. """ + return arg1 % arg2 + + @staticmethod + def power(arg1, arg2): + """ Raise the first argument to the power given by the second """ + return arg1 ** arg2 + + @staticmethod + def square_root(arg1): + """ Return the square root of the argument. If it is a negative + number, raise a NegativeRootError. """ + if arg1 < 0: + raise NegativeRootError(neg_value=arg1) + return sqrt(arg1) + + @staticmethod + def and_(arg1, arg2): + """ Logcially conjoin the two arguments (using short-circuting) """ + return arg1 and arg2 + + @staticmethod + def or_(arg1, arg2): + """ Logically disjoin the two arguments (using short-circuting) """ + return arg1 or arg2 + + @staticmethod + def not_(arg): + """ Return True if the argument evaluates to False, and False + otherwise. """ + return not arg + + @staticmethod + def equals(arg1, arg2): + """ Return arg1 == arg2 """ + return arg1 == arg2 + + @staticmethod + def less(arg1, arg2): + """ Return arg1 < arg2 """ + return arg1 < arg2 + + @staticmethod + def greater(arg1, arg2): + """ Return arg1 > arg2 """ + return arg1 > arg2 + + @staticmethod + def comment(text): + """In 'snail' execution mode, display the comment. Else, do + nothing.""" + tw = global_objects["window"] + if not tw.hide and tw.step_time != 0: + tw.showlabel('print', text) + + @staticmethod + def random_int(lower, upper): + """ Choose a random integer between lower and upper, which must be + integers """ + return int(round(uniform(lower, upper), 0)) + + @staticmethod + def random_char(lower, upper): + """ Choose a random Unicode code point between lower and upper, + which must be integers """ + return chr(Primitive.random_int(lower, upper)) + + +class Disjunction(tuple): + """ Abstract disjunction class (not to be instantiated directly) """ + + def __init__(self, iterable): + self = tuple(iterable) + + def __repr__(self): + s = ["("] + for disj in self: + s.append(repr(disj)) + s.append(" or ") + s.pop() + s.append(")") + return "".join(s) + + def get_alternatives(self): + """ Return a tuple of alternatives, i.e. self """ + return self + + +class PrimitiveDisjunction(Disjunction, Primitive): + """ Disjunction of two or more Primitives. PrimitiveDisjunctions may not + be nested. """ + + @property + def return_type(self): + """ Tuple of the return_types of all disjuncts """ + return TypeDisjunction((prim.return_type for prim in self)) + + def __call__(self, *runtime_args, **runtime_kwargs): + """ Loop over the disjunct Primitives and try to fill their slots + with the given args and kwargs. Call the first Primitives whose + slots could be filled successfully. If all disjunct Primitives + fail, raise the last error that occurred. """ + + # remove the first argument if it is a LogoCode instance + if runtime_args and isinstance(runtime_args[0], LogoCode): + runtime_args = runtime_args[1:] + + error = None + for prim in self: + try: + new_prim = prim.fill_slots(runtime_args, runtime_kwargs, + convert_to_ast=False) + except TATypeError as error: + # on failure, try the next one + continue + else: + # on success, call this Primitive + return new_prim() + + # if we get here, all disjuncts failed + if error is not None: + raise error + + +class ArgListDisjunction(Disjunction): + """ Disjunction of two or more argument lists """ + pass + + +class ArgSlot(object): + """ Description of the requirements that a Primitive demands from an + argument or keyword argument. An ArgSlot is filled at runtime, based + on the block program structure. """ + + def __init__(self, type_, call_arg=True, wrapper=None): + """ + type_ -- what type of the type hierarchy the argument should have + (after the wrapper has been applied) + call_arg -- if this argument is callable, should it be called and + its return value passed to the parent Primitive (True, the + default), or should it be passed as it is (False)? + wrapper -- a Primitive that is 'wrapped around' the argument before + it gets passed to its parent Primitive. Wrappers can be nested + infinitely. """ + self.type = type_ + self.call_arg = call_arg + self.wrapper = wrapper + + def __repr__(self): + s = ["ArgSlot(type="] + s.append(repr(self.type)) + if not self.call_arg: + s.append(", call=") + s.append(repr(self.call_arg)) + if self.wrapper is not None: + s.append(", wrapper=") + s.append(repr(self.wrapper)) + s.append(")") + return "".join(s) + + def get_alternatives(self): + """ Return a tuple of slot alternatives, i.e. (self, ) """ + return (self, ) + + def fill(self, argument, convert_to_ast=False, call_my_args=True): + """ Try to fill this argument slot with the given argument. Return + a ConstantArg containing the result. If there is a type problem, + raise a TATypeError. """ + if isinstance(argument, ast.AST): + convert_to_ast = True + + # 1. can the argument be called? + (func_disjunction, args) = (None, []) + if (isinstance(argument, tuple) and argument + and callable(argument[0])): + func_disjunction = argument[0] + if len(argument) >= 2 and isinstance(argument[1], LogoCode): + args = argument[2:] + else: + args = argument[1:] + elif callable(argument): + func_disjunction = argument + + # make sure we can loop over func_disjunction + if not isinstance(func_disjunction, PrimitiveDisjunction): + func_disjunction = PrimitiveDisjunction((func_disjunction, )) + + error = None + bad_value = argument # the value that caused the TATypeError + for func in func_disjunction: + error = None + for slot in self.get_alternatives(): + + if isinstance(slot.wrapper, PrimitiveDisjunction): + wrapper_disjunction = slot.wrapper + else: + wrapper_disjunction = PrimitiveDisjunction((slot.wrapper,)) + + for wrapper in wrapper_disjunction: + + # check if the argument can fill this slot (type-wise) + # (lambda functions are always accepted) + if getattr(func, '__name__', None) == '': + converter = identity + old_type = TYPE_OBJECT + new_type = slot.type + else: + if wrapper is not None: + arg_types = get_type(wrapper)[0] + bad_value = wrapper + elif func is not None: + arg_types = get_type(func)[0] + bad_value = func + else: + arg_types = get_type(argument)[0] + bad_value = argument + converter = None + if not isinstance(arg_types, TypeDisjunction): + arg_types = TypeDisjunction((arg_types, )) + if isinstance(slot.type, TypeDisjunction): + slot_types = slot.type + else: + slot_types = TypeDisjunction((slot.type, )) + for old_type in arg_types: + for new_type in slot_types: + converter = get_converter(old_type, new_type) + if converter is not None: + break + if converter is not None: + break + # unable to convert, try next wrapper/ slot/ func + if converter is None: + continue + + # 1. (cont'd) call the argument or pass it on as a callable + called_argument = argument + if func is not None: + func_prim = func + if not isinstance(func_prim, Primitive): + func_prim = Primitive( + func_prim, + [ArgSlot(TYPE_OBJECT)] * len(args)) + try: + func_prim = func_prim.fill_slots( + args, + convert_to_ast=convert_to_ast, + call_my_args=(slot.call_arg and call_my_args)) + except TATypeError as error: + if Primitive._DEBUG: + traceback.print_exc() + # on failure, try next wrapper/ slot/ func + bad_value = error.bad_value + continue + if convert_to_ast: + called_argument = func_prim.get_ast() + else: + if slot.call_arg and call_my_args: + # call and pass on the return value + called_argument = func_prim() + else: + # don't call and pass on the callable + called_argument = func_prim + + # 2. apply any wrappers + wrapped_argument = called_argument + if wrapper is not None: + if convert_to_ast: + if not hasattr(wrapper, "get_ast"): + raise PyExportError( + ("cannot convert callable" + " %s to an AST") % (repr(wrapper))) + wrapped_argument = wrapper.get_ast( + called_argument) + else: + if slot.call_arg and call_my_args: + wrapped_argument = wrapper(called_argument) + else: + wrapped_argument = wrapper.fill_slots( + [called_argument], call_my_args=False) + + # last chance to convert raw values to ASTs + # (but not lists of ASTs) + if (convert_to_ast and + not isinstance(wrapped_argument, ast.AST) and + not (isinstance(wrapped_argument, list) and + wrapped_argument and + isinstance(wrapped_argument[0], ast.AST))): + wrapped_argument = value_to_ast(wrapped_argument) + + # 3. check the type and convert the argument if necessary + converted_argument = wrapped_argument + if slot.call_arg and call_my_args: + try: + converted_argument = convert( + wrapped_argument, + new_type, old_type=old_type, + converter=converter) + except TATypeError as error: + if Primitive._DEBUG: + traceback.print_exc() + # on failure, try next wrapper/ slot/ func + bad_value = wrapped_argument + continue + elif converter != identity: + converted_argument = Primitive( + converter, + return_type=new_type, + arg_descs=[ConstantArg(wrapped_argument, + value_type=old_type, + call_arg=False)]) + # on success, return the result + return ConstantArg( + converted_argument, + value_type=new_type, + call_arg=(slot.call_arg and call_my_args)) + + # if we haven't returned anything yet, then all alternatives failed + if error is not None: + raise error + else: + raise TATypeError(bad_value=bad_value, bad_type=old_type, + req_type=new_type) + + +class ArgSlotDisjunction(Disjunction, ArgSlot): + """ Disjunction of two or more argument slots """ + pass + + +class ConstantArg(object): + """ A constant argument or keyword argument to a Primitive. It is + independent of the block program structure. """ + + def __init__(self, value, call_arg=True, value_type=None): + """ call_arg -- call the value before returning it? + value_type -- the type of the value (from the TA type system). This + is useful to store e.g., the return type of call ASTs. """ + self.value = value + self.call_arg = call_arg + self.value_type = value_type + + def get(self, convert_to_ast=False): + """ If call_arg is True and the value is callable, call the value + and return its return value. Else, return the value unchanged. + convert_to_ast -- return the equivalent AST instead of a raw value """ + if self.call_arg and callable(self.value): + if convert_to_ast: + return value_to_ast(self.value) + else: + return self.value() + else: + if convert_to_ast and not isinstance(self.value, list): + return value_to_ast(self.value) + else: + return self.value + + def get_value_type(self): + """ If this ConstantArg has stored the type of its value, return + that. Else, use get_type(...) to guess the type of the value. """ + if self.value_type is None: + return get_type(self.value)[0] + else: + return self.value_type + + def __repr__(self): + s = ["ConstantArg("] + s.append(repr(self.value)) + if not self.call_arg: + s.append(", call=") + s.append(repr(self.call_arg)) + s.append(")") + return "".join(s) + + +def or_(*disjuncts): + """ Return a disjunction object of the same type as the disjuncts. If + the item type cannot be linked to a Disjunction class, return a tuple + of the disjuncts. """ + if isinstance(disjuncts[0], Primitive): + return PrimitiveDisjunction(disjuncts) + elif isinstance(disjuncts[0], (list, ArgListDisjunction)): + return ArgListDisjunction(disjuncts) + elif isinstance(disjuncts[0], ArgSlot): + return ArgSlotDisjunction(disjuncts) + elif isinstance(disjuncts[0], Type): + return TypeDisjunction(disjuncts) + else: + return tuple(disjuncts) + + +def value_to_ast(value, *args_for_prim, **kwargs_for_prim): + """ Turn a value into an AST. Supported types: Primitive, int, float, + bool, basestring, list + If the value is already an AST, return it unchanged. + If the value is a non-exportable Primitive, return None. """ + # already an AST + if isinstance(value, ast.AST): + return value + # Primitive + elif isinstance(value, Primitive): + if value.export_me: + return value.get_ast(*args_for_prim, **kwargs_for_prim) + else: + return None + # boolean + elif isinstance(value, bool): + return ast.Name(id=str(value), ctx=ast.Load) + # number + elif isinstance(value, (int, float)): + return ast.Num(n=value) + # string + elif isinstance(value, basestring): + return ast.Str(value) + # list (recursively transform to an AST) + elif isinstance(value, list): + ast_list = [] + for item in value: + item_ast = value_to_ast(item) + if item_ast is not None: + ast_list.append(item_ast) + return ast.List(elts=ast_list, ctx=ast.Load) + # color + elif isinstance(value, Color): + # call to the Color constructor with this object's values, + # e.g., Color('red', 0, 50, 100) + return get_call_ast('Color', [value.name, value.color, + value.shade, value.gray], + return_type=TYPE_COLOR) + # media + elif isinstance(value, Media): + args = [value_to_ast(value.type), value_to_ast(value.value)] + return get_call_ast('Media', args, return_type=TYPE_MEDIA) + # unknown + else: + raise PyExportError("unknown type of raw value: " + repr(type(value))) + + +def ast_yield_true(): + return ast.Yield(value=ast.Name(id='True', ctx=ast.Load)) + + +def export_me(something): + """ Return True iff this is not a Primitive or its export_me attribute + is True, i.e. everything is exportable except for Primitives with + export_me == False """ + return not isinstance(something, Primitive) or something.export_me diff --git a/TurtleArt/tasprite_factory.py b/TurtleArt/tasprite_factory.py index 2bd8993..8360dbb 100755 --- a/TurtleArt/tasprite_factory.py +++ b/TurtleArt/tasprite_factory.py @@ -609,6 +609,51 @@ stroke-width="3.5" fill="%s" stroke="none" />\n' % (self._stroke) svg += self.footer() return self.header() + svg + def clamp_until(self): + ''' Until block is like clamp but docks are flipped ''' + self.reset_min_max() + x = self._stroke_width / 2.0 + y = self._stroke_width / 2.0 + self._radius + self.margins[0] = int((x + self._stroke_width + 0.5) * self._scale) + self.margins[1] = int((self._stroke_width + 0.5) * self._scale) + self.margins[2] = 0 + self.margins[3] = 0 + svg = self.new_path(x, y) + svg += self._corner(1, -1) + svg += self._do_slot() + svg += self._rline_to(self._radius + self._stroke_width, 0) + svg += self._rline_to(self._expand_x, 0) + xx = self._x + svg += self._corner(1, 1, skip=True) + svg += self._corner(-1, 1, skip=True) + svg += self.line_to(xx, self._y) + svg += self._rline_to(-self._expand_x, 0) + svg += self._do_tab() + svg += self._inverse_corner(-1, 1, 90, 0, 0) + svg += self._rline_to(0, self._expand_y) + svg += self._inverse_corner(1, 1, 90, 0, 0) + svg += self._do_slot() + svg += self._rline_to(self._radius, 0) + if self._innie[0] is True: + svg += self._do_innie() + else: + self.margins[2] = \ + int((self._x - self._stroke_width + 0.5) * self._scale) + svg += self._rline_to(0, self._radius + self._expand_y2) + if self._bool is True: + svg += self._do_boolean() + svg += self._corner(-1, 1) + svg += self._rline_to(-self._radius - self._stroke_width, 0) + svg += self._do_tab() + svg += self._corner(-1, -1) + svg += self._close_path() + self.calc_w_h() + svg += self.style() + if self._collapsible: + svg += self._hide_dot() + svg += self.footer() + return self.header() + svg + def status_block(self, graphic=None): ''' Generate a status block ''' self.reset_min_max() @@ -950,7 +995,8 @@ stroke-width="3.5" fill="%s" stroke="none" />\n' % (self._stroke) 0) return svg_str - def _corner(self, sign_x, sign_y, a=90, l=0, s=1, start=True, end=True): + def _corner(self, sign_x, sign_y, a=90, l=0, s=1, start=True, end=True, + skip=False): svg_str = "" if sign_x == 1 and sign_y == -1: # Upper-left corner self._hide_x = self._x + self._radius + self._stroke_width @@ -970,7 +1016,7 @@ stroke-width="3.5" fill="%s" stroke="none" />\n' % (self._stroke) if start: if sign_x * sign_y == 1: svg_str += self._rline_to(sign_x * r2, 0) - else: + elif not skip: svg_str += self._rline_to(0, sign_y * r2) x = self._x + sign_x * r2 y = self._y + sign_y * r2 @@ -978,7 +1024,7 @@ stroke-width="3.5" fill="%s" stroke="none" />\n' % (self._stroke) if end: if sign_x * sign_y == 1: svg_str += self._rline_to(0, sign_y * r2) - else: + elif not skip: svg_str += self._rline_to(sign_x * r2, 0) return svg_str @@ -1237,6 +1283,7 @@ def close_file(f): def generator(datapath): + ''' svg = SVG() f = open_file(datapath, "turtle.svg") svg.set_scale(2) @@ -1471,29 +1518,42 @@ def generator(datapath): close_file(f) svg = SVG() - f = open_file(datapath, "clampb.svg") + f = open_file(datapath, "clampe.svg") svg.set_scale(2) svg.expand(30, 0, 0, 0) svg.set_slot(True) svg.set_tab(True) svg.set_boolean(True) - svg.second_clamp(False) + svg.second_clamp(True) svg_str = svg.clamp() f.write(svg_str) close_file(f) + ''' svg = SVG() - f = open_file(datapath, "clampe.svg") + f = open_file(datapath, "clampb.svg") svg.set_scale(2) - svg.expand(30, 0, 0, 0) + svg.expand(0, 30, 0, 0) svg.set_slot(True) svg.set_tab(True) svg.set_boolean(True) - svg.second_clamp(True) + svg.second_clamp(False) svg_str = svg.clamp() f.write(svg_str) close_file(f) + svg = SVG() + f = open_file(datapath, "clampu.svg") + svg.set_scale(2) + svg.expand(0, 30, 0, 0) + svg.set_slot(True) + svg.set_tab(True) + svg.set_boolean(True) + svg.second_clamp(False) + svg_str = svg.clamp_until() + f.write(svg_str) + close_file(f) + def main(): return 0 diff --git a/TurtleArt/taturtle.py b/TurtleArt/taturtle.py index ac72bdb..11336d7 100644 --- a/TurtleArt/taturtle.py +++ b/TurtleArt/taturtle.py @@ -28,7 +28,7 @@ import cairo from random import uniform from math import sin, cos, pi, sqrt from taconstants import (TURTLE_LAYER, DEFAULT_TURTLE_COLORS, DEFAULT_TURTLE, - COLORDICT) + Color) from tasprite_factory import SVG, svg_str_to_pixbuf from tacanvas import wrap100, COLOR_TABLE from sprites import Sprite @@ -347,12 +347,7 @@ class Turtle: def set_heading(self, heading, share=True): ''' Set the turtle heading (one shape per 360/SHAPES degrees) ''' - try: - self._heading = heading - except (TypeError, ValueError): - debug_output('bad value sent to %s' % (__name__), - self._turtles.turtle_window.running_sugar) - return + self._heading = heading self._heading %= 360 self._update_sprite_heading() @@ -373,24 +368,18 @@ class Turtle: def set_color(self, color=None, share=True): ''' Set the pen color for this turtle. ''' + if color is None: + color = self._pen_color # Special case for color blocks - if color is not None and color in COLORDICT: - self.set_shade(COLORDICT[color][1], share) - self.set_gray(COLORDICT[color][2], share) - if COLORDICT[color][0] is not None: - self.set_color(COLORDICT[color][0], share) - color = COLORDICT[color][0] + elif isinstance(color, Color): + self.set_shade(color.shade, share) + self.set_gray(color.gray, share) + if color.color is not None: + color = color.color else: color = self._pen_color - elif color is None: - color = self._pen_color - try: - self._pen_color = color - except (TypeError, ValueError): - debug_output('bad value sent to %s' % (__name__), - self._turtles.turtle_window.running_sugar) - return + self._pen_color = color self._turtles.turtle_window.canvas.set_fgcolor(shade=self._pen_shade, gray=self._pen_gray, @@ -404,12 +393,7 @@ class Turtle: def set_gray(self, gray=None, share=True): ''' Set the pen gray level for this turtle. ''' if gray is not None: - try: - self._pen_gray = gray - except (TypeError, ValueError): - debug_output('bad value sent to %s' % (__name__), - self._turtles.turtle_window.running_sugar) - return + self._pen_gray = gray if self._pen_gray < 0: self._pen_gray = 0 @@ -428,12 +412,7 @@ class Turtle: def set_shade(self, shade=None, share=True): ''' Set the pen shade for this turtle. ''' if shade is not None: - try: - self._pen_shade = shade - except (TypeError, ValueError): - debug_output('bad value sent to %s' % (__name__), - self._turtles.turtle_window.running_sugar) - return + self._pen_shade = shade self._turtles.turtle_window.canvas.set_fgcolor(shade=self._pen_shade, gray=self._pen_gray, @@ -447,12 +426,7 @@ class Turtle: def set_pen_size(self, pen_size=None, share=True): ''' Set the pen size for this turtle. ''' if pen_size is not None: - try: - self._pen_size = max(0, pen_size) - except (TypeError, ValueError): - debug_output('bad value sent to %s' % (__name__), - self._turtles.turtle_window.running_sugar) - return + self._pen_size = max(0, pen_size) self._turtles.turtle_window.canvas.set_pen_size( self._pen_size * self._turtles.turtle_window.coord_scale) @@ -547,12 +521,7 @@ class Turtle: def right(self, degrees, share=True): ''' Rotate turtle clockwise ''' - try: - self._heading += degrees - except (TypeError, ValueError): - debug_output('bad value sent to %s' % (__name__), - self._turtles.turtle_window.running_sugar) - return + self._heading += degrees self._heading %= 360 self._update_sprite_heading() @@ -562,6 +531,10 @@ class Turtle: round_int(self._heading)])) self._turtles.turtle_window.send_event(event) + def left(self, degrees, share=True): + degrees = 0 - degrees + self.right(degrees, share) + def _draw_line(self, old, new, pendown): if self._pen_state and pendown: self._turtles.turtle_window.canvas.set_source_rgb() @@ -578,13 +551,8 @@ class Turtle: scaled_distance = distance * self._turtles.turtle_window.coord_scale old = self.get_xy() - try: - xcor = old[0] + scaled_distance * sin(self._heading * DEGTOR) - ycor = old[1] + scaled_distance * cos(self._heading * DEGTOR) - except (TypeError, ValueError): - debug_output('bad value sent to %s' % (__name__), - self._turtles.turtle_window.running_sugar) - return + xcor = old[0] + scaled_distance * sin(self._heading * DEGTOR) + ycor = old[1] + scaled_distance * cos(self._heading * DEGTOR) self._draw_line(old, (xcor, ycor), True) self.move_turtle((xcor, ycor)) @@ -594,19 +562,18 @@ class Turtle: int(distance)])) self._turtles.turtle_window.send_event(event) + def backward(self, distance, share=True): + distance = 0 - distance + self.forward(distance, share) + def set_xy(self, x, y, share=True, pendown=True, dragging=False): old = self.get_xy() - try: - if dragging: - xcor = x - ycor = y - else: - xcor = x * self._turtles.turtle_window.coord_scale - ycor = y * self._turtles.turtle_window.coord_scale - except (TypeError, ValueError): - debug_output('bad value sent to %s' % (__name__), - self._turtles.turtle_window.running_sugar) - return + if dragging: + xcor = x + ycor = y + else: + xcor = x * self._turtles.turtle_window.coord_scale + ycor = y * self._turtles.turtle_window.coord_scale self._draw_line(old, (xcor, ycor), pendown) self.move_turtle((xcor, ycor)) @@ -621,15 +588,10 @@ class Turtle: ''' Draw an arc ''' if self._pen_state: self._turtles.turtle_window.canvas.set_source_rgb() - try: - if a < 0: - pos = self.larc(-a, r) - else: - pos = self.rarc(a, r) - except (TypeError, ValueError): - debug_output('bad value sent to %s' % (__name__), - self._turtles.turtle_window.running_sugar) - return + if a < 0: + pos = self.larc(-a, r) + else: + pos = self.rarc(a, r) self.move_turtle(pos) @@ -691,7 +653,6 @@ class Turtle: def draw_pixbuf(self, pixbuf, a, b, x, y, w, h, path, share=True): ''' Draw a pixbuf ''' - self._turtles.turtle_window.canvas.draw_pixbuf( pixbuf, a, b, x, y, w, h, self._heading) @@ -736,6 +697,19 @@ class Turtle: round_int(w)]])) self._turtles.turtle_window.send_event(event) + def read_pixel(self): + """ Read r, g, b, a from the canvas and push b, g, r to the stack """ + r, g, b, a = self.get_pixel() + self._turtles.turtle_window.lc.heap.append(b) + self._turtles.turtle_window.lc.heap.append(g) + self._turtles.turtle_window.lc.heap.append(r) + + def get_color_index(self): + r, g, b, a = self.get_pixel() + color_index = self._turtles.turtle_window.canvas.get_color_index( + r, g, b) + return color_index + def get_name(self): return self._name diff --git a/TurtleArt/tatype.py b/TurtleArt/tatype.py new file mode 100644 index 0000000..0fcfc3c --- /dev/null +++ b/TurtleArt/tatype.py @@ -0,0 +1,441 @@ +#Copyright (c) 2013 Marion Zepf + +#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. + +""" type system for Primitives and their arguments """ + +import ast + +from tablock import Media +from taconstants import (Color, CONSTANTS) + + +class Type(object): + """ A type in the type hierarchy. """ + + def __init__(self, constant_name, value): + """ constant_name -- the name of the constant that points to this Type + object + value -- an arbitrary integer that is different from the values of + all other Types. The order of the integers doesn't matter. """ + self.constant_name = constant_name + self.value = value + + def __eq__(self, other): + if other is None: + return False + if not isinstance(other, Type): + return False + return self.value == other.value + + def __str__(self): + return str(self.constant_name) + __repr__ = __str__ + + +class TypeDisjunction(tuple,Type): + """ Disjunction of two or more Types (from the type hierarchy) """ + + def __init__(self, iterable): + self = tuple(iterable) + + def __str__(self): + s = ["("] + for disj in self: + s.append(str(disj)) + s.append(" or ") + s.pop() + s.append(")") + return "".join(s) + + +# individual types +TYPE_OBJECT = Type('TYPE_OBJECT', 0) +TYPE_BOOL = Type('TYPE_BOOL', 5) +TYPE_BOX = Type('TYPE_BOX', 8) # special type for the unknown content of a box +TYPE_CHAR = Type('TYPE_CHAR', 1) +TYPE_COLOR = Type('TYPE_COLOR', 2) +TYPE_FLOAT = Type('TYPE_FLOAT', 3) +TYPE_INT = Type('TYPE_INT', 4) +TYPE_MEDIA = Type('TYPE_MEDIA', 10) +TYPE_NUMBER = Type('TYPE_NUMBER', 6) # shortcut to avoid a TypeDisjunction + # between TYPE_FLOAT and TYPE_INT +TYPE_NUMERIC_STRING = Type('TYPE_NUMERIC_STRING', 7) +TYPE_STRING = Type('TYPE_STRING', 9) + +# groups/ classes of types +TYPES_NUMERIC = (TYPE_FLOAT, TYPE_INT, TYPE_NUMBER) + + +BOX_AST = ast.Name(id='BOX', ctx=ast.Load) +ACTION_AST = ast.Name(id='ACTION', ctx=ast.Load) + + +def get_type(x): + """ Return the most specific type in the type hierarchy that applies to x + and a boolean indicating whether x is an AST. If the type cannot be + determined, return TYPE_OBJECT as the type. """ + # non-AST types + if isinstance(x, (int, long)): + return (TYPE_INT, False) + elif isinstance(x, float): + return (TYPE_FLOAT, False) + elif isinstance(x, basestring): + if len(x) == 1: + return (TYPE_CHAR, False) + try: + float(x) + except ValueError: + return (TYPE_STRING, False) + else: + return (TYPE_NUMERIC_STRING, False) + elif isinstance(x, Color): + return (TYPE_COLOR, False) + elif isinstance(x, Media): + return (TYPE_MEDIA, False) + elif hasattr(x, "return_type"): + return (x.return_type, False) + + # AST types + elif isinstance(x, ast.Num): + return (get_type(x.n)[0], True) + elif isinstance(x, ast.Str): + return (get_type(x.s)[0], True) + elif isinstance(x, ast.Name): + try: + # we need to have imported CONSTANTS for this to work + value = eval(x.id) + except NameError: + return (TYPE_OBJECT, True) + else: + return (get_type(value)[0], True) + elif isinstance(x, ast.Subscript): + if x.value == BOX_AST: + return (TYPE_BOX, True) + elif isinstance(x, ast.Call): + if isinstance(x.func, ast.Name): + if x.func.id == 'float': + return (TYPE_FLOAT, True) + elif x.func.id in ('int', 'ord'): + return (TYPE_INT, True) + elif x.func.id == 'chr': + return (TYPE_CHAR, True) + elif x.func.id in ('repr', 'str', 'unicode'): + return (TYPE_STRING, True) + elif x.func.id == 'Color': + return (TYPE_COLOR, True) + elif x.func.id == 'Media': + return (TYPE_MEDIA, True) + # unary operands never change the type of their argument + elif isinstance(x, ast.UnaryOp): + if issubclass(x.op, ast.Not): + # 'not' always returns a boolean + return (TYPE_BOOL, True) + else: + return get_type(x.operand) + # boolean and comparison operators always return a boolean + if isinstance(x, (ast.BoolOp, ast.Compare)): + return (TYPE_BOOL, True) + # other binary operators + elif isinstance(x, ast.BinOp): + type_left = get_type(x.left)[0] + type_right = get_type(x.right)[0] + if type_left == TYPE_STRING or type_right == TYPE_STRING: + return (TYPE_STRING, True) + if type_left == type_right == TYPE_INT: + return (TYPE_INT, True) + else: + return (TYPE_FLOAT, True) + + return (TYPE_OBJECT, isinstance(x, ast.AST)) + + +def is_instancemethod(method): + # TODO how to access the type `instancemethod` directly? + return type(method).__name__ == "instancemethod" + +def is_bound_method(method): + return ((is_instancemethod(method) and method.im_self is not None) or + (hasattr(method, '__self__') and method.__self__ is not None)) + +def is_staticmethod(method): + # TODO how to access the type `staticmethod` directly? + return type(method).__name__ == "staticmethod" + + +def identity(x): + return x + +TYPE_CONVERTERS = { + # Type hierarchy: If there is a converter A -> B, then A is a subtype of B. + # The converter from A to B is stored under TYPE_CONVERTERS[A][B]. + # The relation describing the type hierarchy must be transitive, i.e. + # converting A -> C must yield the same result as converting A -> B -> C. + # TYPE_OBJECT is the supertype of everything. + TYPE_BOX: { + TYPE_FLOAT: float, + TYPE_INT: int, + TYPE_NUMBER: float, + TYPE_STRING: str}, + TYPE_CHAR: { + TYPE_INT: ord, + TYPE_STRING: identity}, + TYPE_COLOR: { + TYPE_FLOAT: float, + TYPE_INT: int, + TYPE_NUMBER: int, + TYPE_STRING: Color.get_number_string}, + TYPE_FLOAT: { + TYPE_INT: int, + TYPE_NUMBER: identity, + TYPE_STRING: str}, + TYPE_INT: { + TYPE_FLOAT: float, + TYPE_NUMBER: identity, + TYPE_STRING: str}, + TYPE_NUMBER: { + TYPE_FLOAT: float, + TYPE_INT: int, + TYPE_STRING: str}, + TYPE_NUMERIC_STRING: { + TYPE_FLOAT: float, + TYPE_STRING: identity} +} + + +class TATypeError(BaseException): + """ TypeError with the types from the hierarchy, not with Python types """ + + def __init__(self, bad_value, bad_type=None, req_type=None, message=''): + """ bad_value -- the mis-typed value that caused the error + bad_type -- the type of the bad_value + req_type -- the type that the value was expected to have + message -- short statement about the cause of the error. It is + not shown to the user, but may appear in debugging output. """ + self.bad_value = bad_value + self.bad_type = bad_type + self.req_type = req_type + self.message = message + + def __str__(self): + msg = [] + if self.message: + msg.append(self.message) + msg.append(" (") + msg.append("bad value: ") + msg.append(repr(self.bad_value)) + if self.bad_type is not None: + msg.append(", bad type: ") + msg.append(repr(self.bad_type)) + if self.req_type is not None: + msg.append(", req type: ") + msg.append(repr(self.req_type)) + if self.message: + msg.append(")") + return "".join(msg) + __repr__ = __str__ + + +def get_converter(old_type, new_type): + """ If there is a converter old_type -> new_type, return it. Else return + None. If a chain of converters is necessary, return it as a tuple or + list (starting with the innermost, first-to-apply converter). """ + # every type can be converted to TYPE_OBJECT + if new_type == TYPE_OBJECT: + return identity + # every type can be converted to itself + if old_type == new_type: + return identity + + # is there a converter for this pair of types? + converters_from_old = TYPE_CONVERTERS.get(old_type) + if converters_from_old is None: + return None + converter = converters_from_old.get(new_type) + if converter is not None: + return converter + else: + # form the transitive closure of all types that old_type can be + # converted to, and look for new_type there + backtrace = converters_from_old.copy() + new_backtrace = backtrace.copy() + break_all = False + while True: + newest_backtrace = {} + for t in new_backtrace: + for new_t in TYPE_CONVERTERS.get(t, {}): + if new_t not in backtrace: + newest_backtrace[new_t] = t + backtrace[new_t] = t + if new_t == new_type: + break_all = True + break + if break_all: + break + if break_all or not newest_backtrace: + break + new_backtrace = newest_backtrace + # use the backtrace to find the path from old_type to new_type + if new_type in backtrace: + converter_chain = [] + t = new_type + while t in backtrace and isinstance(backtrace[t], Type): + converter_chain.insert(0, TYPE_CONVERTERS[backtrace[t]][t]) + t = backtrace[t] + converter_chain.insert(0, TYPE_CONVERTERS[old_type][t]) + return converter_chain + return None + + +def convert(x, new_type, old_type=None, converter=None): + """ Convert x to the new type if possible. + old_type -- the type of x. If not given, it is computed. """ + if not isinstance(new_type, Type): + raise ValueError('%s is not a type in the type hierarchy' + % (repr(new_type))) + # every type can be converted to TYPE_OBJECT + if new_type == TYPE_OBJECT: + return x + if not isinstance(old_type, Type): + (old_type, is_an_ast) = get_type(x) + else: + is_an_ast = isinstance(x, ast.AST) + # every type can be converted to itself + if old_type == new_type: + return x + + # special case: 'box' block (or 'pop' block) as an AST + if is_an_ast and old_type == TYPE_BOX: + new_type_ast = ast.Name(id=new_type.constant_name) + return get_call_ast('convert', [x, new_type_ast], return_type=new_type) + + # if the converter is not given, try to find one + if converter is None: + converter = get_converter(old_type, new_type) + if converter is None: + # no converter available + raise TATypeError(bad_value=x, bad_type=old_type, + req_type=new_type, message=("found no converter" + " for this type combination")) + + def _apply_converter(converter, y): + try: + if is_an_ast: + if converter == identity: + return y + elif is_instancemethod(converter): + func = ast.Attribute(value=y, + attr=converter.im_func.__name__, + ctx=ast.Load) + return get_call_ast(func) + else: + func_name = converter.__name__ + return get_call_ast(func_name, [y]) + else: + return converter(y) + except BaseException: + raise TATypeError(bad_value=x, bad_type=old_type, + req_type=new_type, message=("error during " + "conversion")) + + if isinstance(converter, (list, tuple)): + # apply the converter chain recursively + result = x + for conv in converter: + result = _apply_converter(conv, result) + return result + elif converter is not None: + return _apply_converter(converter, x) + + +class TypedAST(ast.AST): + + @property + def return_type(self): + if self._return_type is None: + return get_type(self.func)[0] + else: + return self._return_type + + +class TypedCall(ast.Call,TypedAST): + """ Like a Call AST, but with a return type """ + + def __init__(self, func, args=None, keywords=None, starargs=None, + kwargs=None, return_type=None): + + if args is None: + args = [] + if keywords is None: + keywords = [] + + ast.Call.__init__(self, func=func, args=args, keywords=keywords, + starargs=starargs, kwargs=kwargs) + + self._return_type = return_type + + +class TypedSubscript(ast.Subscript,TypedAST): + """ Like a Subscript AST, but with a type """ + + def __init__(self, value, slice_, ctx=ast.Load, return_type=None): + + ast.Subscript.__init__(self, value=value, slice=slice_, ctx=ctx) + + self._return_type = return_type + + +class TypedName(ast.Name,TypedAST): + """ Like a Name AST, but with a type """ + + def __init__(self, id_, ctx=ast.Load, return_type=None): + + ast.Name.__init__(self, id=id_, ctx=ctx) + + self._return_type = return_type + + +def get_call_ast(func_name, args=None, kwargs=None, return_type=None): + """ Return an AST representing the call to a function with the name + func_name, passing it the arguments args (given as a list) and the + keyword arguments kwargs (given as a dictionary). + func_name -- either the name of a callable as a string, or an AST + representing a callable expression + return_type -- if this is not None, return a TypedCall object with this + return type instead """ + if args is None: + args = [] + # convert keyword argument dict to a list of (key, value) pairs + keywords = [] + if kwargs is not None: + for (key, value) in kwargs.iteritems(): + keywords.append(ast.keyword(arg=key, value=value)) + # get or generate the AST representing the callable + if isinstance(func_name, ast.AST): + func_ast = func_name + else: + func_ast = ast.Name(id=func_name, ctx=ast.Load) + # if no return type is given, return a simple Call AST + if return_type is None: + return ast.Call(func=func_ast, args=args, keywords=keywords, + starargs=None, kwargs=None) + # if a return type is given, return a TypedCall AST + else: + return TypedCall(func=func_ast, args=args, keywords=keywords, + return_type=return_type) diff --git a/TurtleArt/tautils.py b/TurtleArt/tautils.py index b0aa368..ff30b19 100644 --- a/TurtleArt/tautils.py +++ b/TurtleArt/tautils.py @@ -51,7 +51,7 @@ except (ImportError, AttributeError): from StringIO import StringIO from taconstants import (HIT_HIDE, HIT_SHOW, XO1, XO15, XO175, XO4, UNKNOWN, - MAGICNUMBER, SUFFIX) + MAGICNUMBER, SUFFIX, ARG_MUST_BE_NUMBER) import logging _logger = logging.getLogger('turtleart-activity') @@ -107,7 +107,7 @@ 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 + return ord(x), True except ValueError: return x, False return x, False @@ -115,9 +115,7 @@ def chr_to_ord(x): def strtype(x): ''' Is x a string type? ''' - if isinstance(x, (str, unicode)): - return True - return False + return isinstance(x, basestring) def increment_name(name): @@ -311,7 +309,7 @@ def get_save_name(filefilter, load_save_folder, save_file_name): 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 filefilter in ['.png', '.svg', '.lg']: + if filefilter in ['.png', '.svg', '.lg', '.py']: suffix = filefilter else: suffix = SUFFIX[1] @@ -679,14 +677,11 @@ def arithmetic_check(blk1, blk2, dock1, dock2): 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']: + elif blk1.name in ARG_MUST_BE_NUMBER and blk2.name in ARG_MUST_BE_NUMBER: if blk1.name == 'string': if not numeric_arg(blk1.values[0]): return False - elif blk1.name == 'string': + elif blk2.name == 'string': if not numeric_arg(blk2.values[0]): return False elif blk1.name in ['greater2', 'less2'] and blk2.name == 'string': @@ -808,6 +803,30 @@ def find_blk_below(blk, namelist): return None +def get_stack_name(blk): + ''' Return the name of the action stack that the given block belongs to. + If the top block of this stack is not a stack-defining block, return + None. ''' + top_block = find_top_block(blk) + if top_block.name == 'start': + return 'start' + elif top_block.name == 'hat1': + return 'stack1' + elif top_block.name == 'hat2': + return 'stack2' + elif top_block.name == 'hat': + try: + return str(top_block.connections[1].values[0]) + except (AttributeError, TypeError, IndexError): + # AttributeError: t_b has no attribute 'connections' or t_b.c[1] + # has no attribute 'value' + # TypeError: t_b.c or t_b.c[1].v is not a subscriptable sequence + # IndexError: t_b.c or t_b.c[1].v is too short + return None + else: + return None + + def get_hardware(): ''' Determine whether we are using XO 1.0, 1.5, ... or 'unknown' hardware ''' diff --git a/TurtleArt/tawindow.py b/TurtleArt/tawindow.py index 5072ab2..ae5fb97 100644 --- a/TurtleArt/tawindow.py +++ b/TurtleArt/tawindow.py @@ -58,16 +58,17 @@ from taconstants import (HORIZONTAL_PALETTE, VERTICAL_PALETTE, BLOCK_SCALE, PYTHON_SKIN, PALETTE_HEIGHT, STATUS_LAYER, OLD_DOCK, EXPANDABLE_ARGS, XO1, XO15, XO175, XO30, XO4, TITLEXY, CONTENT_ARGS, CONSTANTS, EXPAND_SKIN, PROTO_LAYER, - EXPANDABLE_FLOW, SUFFIX, TMP_SVG_PATH) + EXPANDABLE_FLOW, SUFFIX, TMP_SVG_PATH, Color, + KEY_DICT) from tapalette import (palette_names, palette_blocks, expandable_blocks, block_names, content_blocks, default_values, special_names, block_styles, help_strings, hidden_proto_blocks, string_or_number_args, make_palette, palette_name_to_index, - palette_init_on_start) -from talogo import (LogoCode, primitive_dictionary, logoerror) + palette_init_on_start, palette_i18n_names) +from talogo import (LogoCode, logoerror) from tacanvas import TurtleGraphics -from tablock import (Blocks, Block) +from tablock import (Blocks, Block, Media, media_blocks_dictionary) from taturtle import (Turtles, Turtle) from tautils import (magnitude, get_load_name, get_save_name, data_from_file, data_to_file, round_int, get_id, get_pixbuf_from_journal, @@ -77,10 +78,11 @@ from tautils import (magnitude, get_load_name, get_save_name, data_from_file, find_block_to_run, find_top_block, journal_check, find_group, find_blk_below, data_to_string, find_start_stack, get_hardware, debug_output, - error_output, convert, find_hat, find_bot_block, + error_output, find_hat, find_bot_block, restore_clamp, collapse_clamp, data_from_string, increment_name, get_screen_dpi) from tasprite_factory import (SVG, svg_str_to_pixbuf, svg_from_file) +from tapalette import block_primitives from sprites import (Sprites, Sprite) if _GST_AVAILABLE: @@ -95,6 +97,10 @@ _UNFULLSCREEN_VISIBILITY_TIMEOUT = 2 _PLUGIN_SUBPATH = 'plugins' _MACROS_SUBPATH = 'macros' +# the global instances of single-instance classes +global_objects = {} +plugins_in_use = [] + class TurtleArtWindow(): ''' TurtleArt Window class abstraction ''' @@ -168,6 +174,7 @@ class TurtleArtWindow(): self._autohide_shape = True self.keypress = '' self.keyvalue = 0 + self.keyboard = 0 self._focus_out_id = None self._insert_text_id = None self._text_to_check = False @@ -246,6 +253,7 @@ class TurtleArtWindow(): self.selected_selector = None self.previous_selector = None self.selector_shapes = [] + self._highlighted_blk = None self.selected_blk = None self.selected_spr = None self.selected_turtle = None @@ -300,12 +308,26 @@ class TurtleArtWindow(): from tabasics import Palettes self._basic_palettes = Palettes(self) + global_objects["window"] = self + global_objects["canvas"] = self.canvas + global_objects["logo"] = self.lc + global_objects["turtles"] = self.turtles + if self.interactive_mode: gobject.idle_add(self._lazy_init) else: self._init_plugins() self._setup_plugins() + def get_global_objects(self): + return global_objects + + def get_init_complete(self): + if self.running_turtleart: + return self.activity.init_complete + else: + return True + def _lazy_init(self): self._init_plugins() self._setup_plugins() @@ -325,8 +347,9 @@ class TurtleArtWindow(): regenerate=True, show=True) - if self.running_sugar: - self.activity.check_buttons_for_fit() + if self.running_sugar: + self.activity.check_buttons_for_fit() + self.activity.update_palette_from_metadata() def _set_screen_dpi(self): dpi = get_screen_dpi() @@ -379,10 +402,11 @@ class TurtleArtWindow(): # Add the icon dir to the icon_theme search path self._add_plugin_icon_dir(os.path.join(self._get_plugin_home(), plugin_dir)) + # Add the plugin to the list of global objects + global_objects[plugin_class] = self.turtleart_plugins[-1] except Exception as e: debug_output('Failed to load %s: %s' % (plugin_class, str(e)), self.running_sugar) - def _add_plugin_icon_dir(self, dirname): ''' If there is an icon subdir, add it to the search path. ''' @@ -413,7 +437,7 @@ class TurtleArtWindow(): # If setup fails, remove the plugin from the list self.turtleart_plugins.remove(plugin) - def _start_plugins(self): + def start_plugins(self): ''' Start is called everytime we execute blocks. ''' for plugin in self.turtleart_plugins: if hasattr(plugin, 'start'): @@ -762,6 +786,9 @@ class TurtleArtWindow(): self.draw_overlay('Cartesian') return + def get_coord_scale(self): + return self.coord_scale + def set_polar(self, flag): ''' Turn on/off polar coordinates ''' self.draw_overlay('polar') @@ -1477,11 +1504,25 @@ class TurtleArtWindow(): self.button_press(event.get_state() & gtk.gdk.CONTROL_MASK, x, y) return True + def get_mouse_flag(self): + return self.mouse_flag + + def get_mouse_button(self): + return self.mouse_flag == 1 + + def get_mouse_x(self): + return int(self.mouse_x - (self.canvas.width / 2)) + + def get_mouse_y(self): + return int((self.canvas.height / 2) - self.mouse_y) + def button_press(self, mask, x, y): - if self.running_sugar: - self._show_unfullscreen_button() + if self.running_turtleart: + if self.running_sugar: + self._show_unfullscreen_button() - self.activity.hide_store() + if self.interactive_mode: + self.activity.hide_store() # Find out what was clicked spr = self.sprite_list.find_sprite((x, y)) @@ -1491,7 +1532,7 @@ class TurtleArtWindow(): blk = self.block_list.spr_to_block(spr) if blk is not None: # Make sure stop button is visible - if self.running_sugar: + if self.running_sugar and self.running_turtleart: self.activity.stop_turtle_button.set_icon("stopiton") self.activity.stop_turtle_button.set_tooltip( _('Stop turtle')) @@ -1562,9 +1603,10 @@ before making changes to your program')) self.status_spr.hide() self._autohide_shape = True - def _look_for_a_blk(self, spr, x, y): + def _look_for_a_blk(self, spr, x, y, blk=None): # From the sprite at x, y, look for a corresponding block - blk = self.block_list.spr_to_block(spr) + if blk is None: + blk = self.block_list.spr_to_block(spr) ''' If we were copying and didn't click on a block... ''' if self.copying_blocks or self.sharing_blocks or self.saving_blocks: if blk is None or blk.type != 'block': @@ -1586,9 +1628,9 @@ before making changes to your program')) self._restore_from_trash(find_top_block(blk)) elif blk.type == 'proto': if self.deleting_blocks: - if 'my blocks' in palette_names and \ + if 'myblocks' in palette_names and \ self.selected_palette == \ - palette_names.index('my blocks'): + palette_names.index('myblocks'): self._delete_stack_alert(blk) self.parent.get_window().set_cursor( gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)) @@ -1598,10 +1640,7 @@ before making changes to your program')) elif blk.name == 'restore': self.restore_latest_from_trash() elif blk.name == 'empty': - if self.running_sugar: - self.activity.empty_trash_alert() - else: - self.empty_trash() + self.empty_trash() elif blk.name == 'trashall': for b in self.just_blocks(): if b.type != 'trash': @@ -1623,9 +1662,20 @@ before making changes to your program')) name = blk.name # You can only have one instance of some blocks if blk.name in ['start', 'hat1', 'hat2']: - if len(self.block_list.get_similar_blocks( - 'block', blk.name)) > 0: + blk_list = self.block_list.get_similar_blocks( + 'block', blk.name) + if len(blk_list) > 0: self.showlabel('dupstack') + if blk.name == 'start': + # Recenter the screen and move the start + # stack to the center of the screen + if self.running_sugar: + self.activity.recenter() + dx = 200 - blk_list[0].spr.get_xy()[0] + dy = 200 - blk_list[0].spr.get_xy()[1] + drag_group = find_group(blk_list[0]) + for dblk in drag_group: + dblk.spr.move_relative((dx, dy)) return True # We need to check to see if there is already a # similarly default named stack @@ -1797,7 +1847,7 @@ before making changes to your program')) error_output('Could not remove macro %s: %s' % (macro_path, e)) return - i = palette_names.index('my blocks') + i = palette_names.index('myblocks') palette_blocks[i].remove(blk.name) for pblk in self.palettes[i]: if pblk.name == blk.name: @@ -1854,17 +1904,21 @@ before making changes to your program')) elif spr.name == _('shift'): self._shift_toolbar_palette(self.selected_palette) else: - self.orientation = 1 - self.orientation - self.palette_button[self.orientation].set_layer(TAB_LAYER) - self.palette_button[1 - self.orientation].hide() - self.palette_sprs[self.selected_palette][ - 1 - self.orientation].hide() - self._layout_palette(self.selected_palette) - self.show_palette(self.selected_palette) + self.set_orientation(1 - self.orientation) elif spr.type == 'toolbar': self._select_toolbar_button(spr) return False + def set_orientation(self, orientation): + self.orientation = orientation + self.palette_button[self.orientation].set_layer(TAB_LAYER) + self.palette_button[1 - self.orientation].hide() + spr = self.palette_sprs[self.selected_palette][1 - self.orientation] + if spr is not None: + spr.hide() + self._layout_palette(self.selected_palette) + self.show_palette(self.selected_palette) + def _update_action_names(self, name): ''' change the label on action blocks of the same name ''' if isinstance(name, (float, int)): @@ -2133,7 +2187,7 @@ before making changes to your program')) for gblk in group: if gblk.name == 'sandwichclampcollapsed': restore_clamp(gblk) - self.resize_parent_clamps(gblk) + self._resize_parent_clamps(gblk) for gblk in group: gblk.rescale(self.block_scale) @@ -2157,10 +2211,27 @@ before making changes to your program')) def empty_trash(self): ''' Permanently remove all blocks presently in the trash can. ''' + title = _('empty trash') + msg = _('Do you really want to empty the trash?') + if self.running_sugar: + self.activity.empty_trash_alert(title, msg) + else: + dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_WARNING, + gtk.BUTTONS_OK_CANCEL, msg) + dialog.set_title(title) + res = dialog.run() + dialog.destroy() + if res == gtk.RESPONSE_OK: + self._empty_trash() + + def _empty_trash(self): + remove_list = [] for blk in self.block_list.list: if blk.type == 'trash': - blk.type = 'deleted' blk.spr.hide() + remove_list.append(blk) + for blk in remove_list: + self.block_list.list.remove(blk) self.trash_stack = [] if 'trash' in palette_names: self.show_toolbar_palette(palette_names.index('trash'), @@ -2482,10 +2553,15 @@ before making changes to your program')) if blk is None: continue if blk.name in EXPANDABLE_FLOW: - if blk.name in block_styles['clamp-style-1arg'] or\ + if blk.name in block_styles['clamp-style-1arg'] or \ blk.name in block_styles['clamp-style-boolean']: if blk.connections[2] is not None: self._resize_clamp(blk, blk.connections[2]) + elif blk.name in block_styles['clamp-style-until']: + if blk.connections[2] is not None: + self._resize_clamp(blk, blk.connections[2]) + if blk.connections[1] is not None: + self._resize_clamp(blk, blk.connections[1], dockn=1) elif blk.name in block_styles['clamp-style']: if blk.connections[1] is not None: self._resize_clamp(blk, blk.connections[1]) @@ -3157,6 +3233,13 @@ before making changes to your program')) if len(self.block_list.get_similar_blocks('block', 'forever')) > 0: debug_output('WARNING: Projects with forever blocks \ may not terminate.', False) + else: + self._hide_text_entry() + self.parent.get_window().set_cursor( + gtk.gdk.Cursor(gtk.gdk.WATCH)) + gobject.idle_add(self.__run_stack, blk) + + def __run_stack(self, blk): if self.status_spr is not None: self.status_spr.hide() self._autohide_shape = True @@ -3166,15 +3249,19 @@ before making changes to your program')) if self.canvas.cr_svg is None: self.canvas.setup_svg_surface() self.running_blocks = True - self._start_plugins() # Let the plugins know we are running. + self.start_plugins() # Let the plugins know we are running. top = find_top_block(blk) code = self.lc.generate_code(top, self.just_blocks()) + if self.interactive_mode: + self.parent.get_window().set_cursor( + gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)) self.lc.run_blocks(code) if self.interactive_mode: gobject.idle_add(self.lc.doevalstep) else: while self.lc.doevalstep(): pass + self.running_blocks = False def _snap_to_dock(self): ''' Snap a block (selected_block) to the dock of another block @@ -3313,6 +3400,14 @@ before making changes to your program')) if best_destination_dockn == 2: self._resize_clamp(best_destination, self.drag_group[0]) + elif best_destination.name in \ + block_styles['clamp-style-until']: + if best_destination_dockn == 2: + self._resize_clamp(best_destination, + self.drag_group[0]) + elif best_destination_dockn == 1: + self._resize_clamp(best_destination, + self.drag_group[0], dockn=1) elif best_destination.name in block_styles['clamp-style'] or \ best_destination.name in \ block_styles['clamp-style-collapsible']: @@ -3410,16 +3505,19 @@ before making changes to your program')) self._expand_expandable(blk2, blk, dy) self._cascade_expandable(blk2) elif c is not None and blk2.name in EXPANDABLE_FLOW: - if blk2.name in block_styles['clamp-style-1arg'] or\ + if blk2.name in block_styles['clamp-style-1arg'] or \ blk2.name in block_styles['clamp-style-boolean']: if c == 2: - self._resize_clamp(blk2, None, c) + self._resize_clamp(blk2, None, dockn=c) + elif blk2.name in block_styles['clamp-style-until']: + if c in [1, 2]: + self._resize_clamp(blk2, None, dockn=c) elif blk2.name in block_styles['clamp-style'] or \ blk2.name in block_styles['clamp-style-collapsible']: if c == 1: self._resize_clamp(blk2, None) elif blk2.name in block_styles['clamp-style-else']: - if c == 2 or c == 3: + if c in [2, 3]: self._resize_clamp(blk2, None, dockn=c) while blk3 is not None and blk3.connections[dockn] is not None: self._resize_clamp(blk3, blk3.connections[dockn], dockn=dockn) @@ -3435,22 +3533,35 @@ before making changes to your program')) y1 = blk.docks[-1][3] if blk.name in block_styles['clamp-style-else'] and dockn == 3: blk.reset_y2() + elif blk.name in block_styles['clamp-style-until'] and dockn == 1: + blk.reset_y2() else: blk.reset_y() dy = 0 # Calculate height of drag group - while gblk is not None: - delta = int((gblk.docks[-1][3] - gblk.docks[0][3]) / gblk.scale) - if delta == 0: - dy += 21 # Fixme: don't hardcode size of stop action block - else: - dy += delta - gblk = gblk.connections[-1] - # Clamp has room for one 'standard' block by default - if dy > 0: - dy -= 21 # Fixme: don't hardcode + if blk.name in block_styles['clamp-style-until'] and dockn == 1: + if gblk is not None: + dy = int(gblk.spr.rect.height / gblk.scale) + # Room for part of one 'standard' boolean by default + if dy > 0: + dy -= 25 # Fixme: don't hardcode size of slot + if dy < 0: + dy = 0 + else: + while gblk is not None: + delta = int((gblk.docks[-1][3] - gblk.docks[0][3]) / gblk.scale) + if delta == 0: + dy += 21 # Fixme: don't hardcode size of slot + else: + dy += delta + gblk = gblk.connections[-1] + # Clamp has room for one 'standard' block by default + if dy > 0: + dy -= 21 # Fixme: don't hardcode size of slot if blk.name in block_styles['clamp-style-else'] and dockn == 3: blk.expand_in_y2(dy) + elif blk.name in block_styles['clamp-style-until'] and dockn == 1: + blk.expand_in_y2(dy) else: blk.expand_in_y(dy) y2 = blk.docks[-1][3] @@ -3460,12 +3571,18 @@ before making changes to your program')) drag_group = find_group(blk.connections[-1]) for gblk in drag_group: gblk.spr.move_relative((0, y2-y1)) - # We may have to move the else clamp group down too. + # We may have to move the else clamp group up or down too. if blk.name in block_styles['clamp-style-else'] and dockn == 2: if blk.connections[3] is not None: drag_group = find_group(blk.connections[3]) for gblk in drag_group: gblk.spr.move_relative((0, y2 - y1)) + # We may have to move the bool group up or down too. + if blk.name in block_styles['clamp-style-until']: + if blk.connections[1] is not None: + drag_group = find_group(blk.connections[1]) + for gblk in drag_group: + gblk.spr.move_relative((0, y2 - y1)) def _expandable_flow_above(self, blk): ''' Is there an expandable flow block above this one? ''' @@ -3584,15 +3701,68 @@ before making changes to your program')) elif keyname == 'g': self._align_to_grid() - elif self.selected_blk is not None and \ - self.selected_blk.name != 'proto': - self._process_keyboard_commands(keyname, block_flag=True) + elif keyname == 'Tab': + # For the first pass, just tab through palettes + if self.selected_palette is None: + print 'selected palette is None' + return True + else: + p = self.palettes[self.selected_palette] + i = 0 + if self._highlighted_blk is not None: + self._highlighted_blk.unhighlight() + if self._highlighted_blk in p: + i = p.index(self._highlighted_blk) + if i == len(p) - 1: + i = 0 + else: + i += 1 + while(not p[i].get_visibility()): + i += 1 + if i >= len(p) - 1: + i = 0 + if i < len(p): + self._highlighted_blk = p[i] + self._highlighted_blk.highlight() + self.selected_blk = p[i] + + elif self.selected_blk is not None: + if self.selected_blk.type == 'proto': + if keyname == 'Return': + (x, y) = self.selected_blk.spr.get_xy() + if self.orientation == 0: + x += 20 + y += PALETTE_HEIGHT + self.toolbar_offset + else: + x += PALETTE_WIDTH + y += 20 + self._look_for_a_blk(None, x, y, blk=self.selected_blk) + self._unselect_all_blocks() + self.selected_spr = None + self.drag_group = None + else: + self._process_keyboard_commands(keyname, block_flag=True) elif self.turtles.spr_to_turtle(self.selected_spr) is not None: self._process_keyboard_commands(keyname, block_flag=False) return True + def get_keyboard_input(self): + """ Query keyboard and update cached keyboard input """ + self.window.grab_focus() + if len(self.keypress) == 1: + self.keyboard = ord(self.keypress[0]) + elif self.keypress in KEY_DICT: + self.keyboard = KEY_DICT[self.keypress] + else: + self.keyboard = 0 + self.keypress = '' + + def get_keyboard(self): + """ Return cached keyboard input """ + return self.keyboard + def _process_keyboard_commands(self, keyname, block_flag=True): ''' Use the keyboard to move blocks and turtle ''' mov_dict = {'KP_Up': [0, 20], 'j': [0, 20], 'Up': [0, 20], @@ -3631,7 +3801,7 @@ before making changes to your program')) if block is not None: self.selected_spr = block.spr block.highlight() - else: + elif blk is not None: self._jog_block(blk, mov_dict[keyname][0], mov_dict[keyname][1]) elif not block_flag: @@ -3697,11 +3867,8 @@ before making changes to your program')) self._snap_to_dock() self.drag_group = None - def _test_number(self): - ''' Make sure a 'number' block contains a number. ''' + def _hide_text_entry(self): if hasattr(self, '_text_entry'): - bounds = self._text_buffer.get_bounds() - text = self._text_buffer.get_text(bounds[0], bounds[1]) if self._focus_out_id is not None: self._text_entry.disconnect(self._focus_out_id) self._focus_out_id = None @@ -3709,6 +3876,13 @@ before making changes to your program')) self._text_buffer.disconnect(self._insert_text_id) self._insert_text_id = None self._text_entry.hide() + + def _test_number(self): + ''' Make sure a 'number' block contains a number. ''' + if hasattr(self, '_text_entry'): + bounds = self._text_buffer.get_bounds() + text = self._text_buffer.get_text(bounds[0], bounds[1]) + self._hide_text_entry() else: text = self.selected_blk.spr.labels[0] self._number_check(text) @@ -3755,12 +3929,9 @@ before making changes to your program')) def _test_string(self): if hasattr(self, '_text_entry'): - if self._focus_out_id is not None: - self._text_entry.disconnect(self._focus_out_id) - self._focus_out_id = None bounds = self._text_buffer.get_bounds() text = self._text_buffer.get_text(bounds[0], bounds[1]) - self._text_entry.hide() + self._hide_text_entry() else: text = self.selected_blk.spr.labels[0] self.selected_blk.spr.set_label(text.replace('\12', RETURN)) @@ -3908,8 +4079,6 @@ before making changes to your program')) self.new_project() self.process_data(data_from_file(ta_file)) self._loaded_project = ta_file - # Always start on the Turtle palette - self.show_toolbar_palette(palette_name_to_index('turtle')) def load_file_from_chooser(self, create_new_project=True): ''' Load a project from file chooser ''' @@ -4349,6 +4518,60 @@ before making changes to your program')) elif self.interactive_mode: self.parent.set_title(text) + def print_(self, n, flag): + """ Print object n to the bar at the bottom of the screen """ + if flag and (self.hide or self.step_time == 0): + return + + # list + if isinstance(n, list): + heap_as_string = str(self.lc.heap) + if len(heap_as_string) > 80: + self.showlabel('print', str(self.lc.heap)[0:79] + '…') + else: + self.showlabel('print', str(self.lc.heap)) + # color + elif isinstance(n, Color): + if n.color is None: + self.showlabel('print', '%s %d, %s %d' % + (_('shade'), n.shade, + _('gray'), n.gray)) + else: + self.showlabel('print', '%s %d, %s %d, %s %d' % + (_('color'), n.color, + _('shade'), n.shade, + _('gray'), n.gray)) + # media + elif isinstance(n, Media): + if (n.type == 'media' and + n.value.lower() not in media_blocks_dictionary): + try: + if self.running_sugar: + from sugar.datastore import datastore + try: + dsobject = datastore.get(n.value) + except: + debug_output("Couldn't open %s" % (n.value), + self.running_sugar) + self.showlabel('print', dsobject.metadata['title']) + dsobject.destroy() + else: + self.showlabel('print', n.value) + except IOError: + self.showlabel('print', str(n)) + else: + self.showlabel('print', str(n)) + # string + elif isinstance(n, basestring): + self.showlabel('print', n) + # integer + elif isinstance(n, int): + self.showlabel('print', n) + # other number + else: + self.showlabel( + 'print', + str(round_int(n)).replace('.', self.decimal_point)) def showlabel(self, shp, label=''): ''' Display a message on a status block ''' @@ -4371,6 +4594,9 @@ before making changes to your program')) elif shp[0] == '#': shp = shp[1:] label = '' + if self.running_sugar and \ + shp not in ['print', 'status', 'info', 'help']: + self.activity.error_list.append(shp) self.status_spr.set_shape(self.status_shapes[shp]) self.status_spr.set_label_attributes(12.0, rescale=False) if shp == 'status': @@ -4384,13 +4610,20 @@ before making changes to your program')) self.status_spr.move((PALETTE_WIDTH, self.height - 400)) else: # Adjust vertical position based on scrolled window adjustment + offset_from_bottom = 60 if self.running_sugar: + if self.activity.toolbox.get_property("visible"): + if self.activity.toolbars_expanded(): + offset_from_bottom += 110 + else: + offset_from_bottom += 60 self.status_spr.move( (0, - self.height - 200 + + self.height - offset_from_bottom + self.activity.sw.get_vadjustment().get_value())) elif self.interactive_mode: - self.status_spr.move((0, self.height - 100)) + self.status_spr.move( + (0, self.activity.win.get_window().get_size()[1] - 80)) def calc_position(self, template): ''' Relative placement of portfolio objects (deprecated) ''' @@ -4620,7 +4853,6 @@ before making changes to your program')) palette = make_palette('blocks') # Create a new block prototype. - primitive_dictionary['stack'] = self._prim_stack palette.add_block('stack_%s' % (name), style='basic-style-1arg', label=name, @@ -4629,7 +4861,6 @@ before making changes to your program')) logo_command='action', default=name, help_string=_('invokes named action stack')) - self.lc.def_prim('stack', 1, primitive_dictionary['stack'], True) # Regenerate the palette, which will now include the new block. self.show_toolbar_palette(palette_name_to_index('blocks'), @@ -4649,7 +4880,6 @@ before making changes to your program')) palette = make_palette('blocks') # Create a new block prototype. - primitive_dictionary['box'] = self._prim_box palette.add_block('box_%s' % (name), style='number-style-1strarg', label=name, @@ -4658,8 +4888,6 @@ before making changes to your program')) default=name, logo_command='box', help_string=_('named variable (numeric value)')) - self.lc.def_prim('box', 1, - lambda self, x: primitive_dictionary['box'](x)) # Regenerate the palette, which will now include the new block. self.show_toolbar_palette(palette_name_to_index('blocks'), @@ -4679,7 +4907,6 @@ before making changes to your program')) palette = make_palette('blocks') # Create a new block prototype. - primitive_dictionary['setbox'] = self._prim_setbox palette.add_block('storein_%s' % (name), style='basic-style-2arg', label=[_('store in'), name, _('value')], @@ -4689,52 +4916,11 @@ before making changes to your program')) default=[name, 100], help_string=_('stores numeric value in named \ variable')) - self.lc.def_prim( - 'storeinbox', - 2, - lambda self, x, y: primitive_dictionary['setbox']('box3', x, y)) # Regenerate the palette, which will now include the new block. self.show_toolbar_palette(palette_name_to_index('blocks'), regenerate=True) - def _prim_stack(self, x): - ''' Process a named stack ''' - if isinstance(convert(x, float, False), float): - if int(float(x)) == x: - x = int(x) - if 'stack3' + str(x) not in self.lc.stacks or \ - self.lc.stacks['stack3' + str(x)] is None: - raise logoerror('#nostack') - self.lc.icall(self.lc.evline, - self.lc.stacks['stack3' + str(x)][:]) - yield True - self.lc.procstop = False - self.lc.ireturn() - yield True - - def _prim_box(self, x): - ''' Retrieve value from named box ''' - if isinstance(convert(x, float, False), float): - if int(float(x)) == x: - x = int(x) - try: - return self.lc.boxes['box3' + str(x)] - except KeyError: - raise logoerror('#emptybox') - - def _prim_setbox(self, name, x, val): - ''' Define value of named box ''' - if x is not None: - if isinstance(convert(x, float, False), float): - if int(float(x)) == x: - x = int(x) - self.lc.boxes[name + str(x)] = val - self.lc.update_label_value('box', val, label=x) - else: - self.lc.boxes[name] = val - self.lc.update_label_value(name, val) - def dock_dx_dy(self, block1, dock1n, block2, dock2n): ''' Find the distance between the dock points of two blocks. ''' # Cannot dock a block to itself @@ -4786,3 +4972,100 @@ variable')) (b1x, b1y) = block1.spr.get_xy() (b2x, b2y) = block2.spr.get_xy() return ((b1x + d1x) - (b2x + d2x), (b1y + d1y) - (b2y + d2y)) + + def prim_load_block(self, *args): + ''' Load a block on to the canvas ''' + # Place the block at the active turtle (x, y) and move the turtle + # into position to place the next block in the stack. + # TODO: Add expandable argument, media block arguments + name = args[0] + pos = self.turtles.get_active_turtle().get_xy() + values = [] + for i in range(len(args) - 1): + values.append(args[i + 1]) + if len(values) > 0: + dy = int(self._find_block(name, pos[0], pos[1], values)) + else: + if name == 'delete': + for blk in self.just_blocks(): + if blk.status == 'load block': + blk.type = 'trash' + blk.spr.hide() + dy = 0 + else: + dy = int(self._find_block(name, pos[0], pos[1])) + + # Reposition turtle to end of flow + pos = self.turtles.get_active_turtle().get_xy() + pos[1] -= dy + self.turtles.get_active_turtle().move_turtle(pos) + + def _make_block(self, name, x, y, defaults): + if defaults is None: + self._new_block(name, x, y, defaults) + else: + for i, v in enumerate(defaults): + if isinstance(v, float) and int(v) == v: + defaults[i] = int(v) + self._new_block(name, x, y, defaults) + + # Find the block we just created and attach it to a stack. + self.drag_group = None + spr = self.sprite_list.find_sprite((x, y)) + if spr is not None: + blk = self.block_list.spr_to_block(spr) + if blk is not None: + self.drag_group = find_group(blk) + for b in self.drag_group: + b.status = 'load block' + self._snap_to_dock() + + # Disassociate new block from mouse. + self.drag_group = None + return blk.docks[-1][3] + + def _find_block(self, blkname, x, y, defaults=None): + """ Create a new block. It is a bit more work than just calling + _new_block(). We need to: + (1) translate the label name into the internal block name; + (2) 'dock' the block onto a stack where appropriate; and + (3) disassociate the new block from the mouse. """ + x, y = self.turtles.turtle_to_screen_coordinates((x, y)) + for name in block_names: + # Translate label name into block/prim name. + if blkname in block_names[name]: # block label is an array + # print 'found a match', blkname, name, block_names[name] + if name in content_blocks or \ + (name in block_primitives and + block_primitives[name] == name): + # print '_make_block', blkname, name + return self._make_block(name, x, y, defaults) + elif blkname in block_names: + # print '_make_block', blkname + return self._make_block(blkname, x, y, defaults) + for name in special_names: + # Translate label name into block/prim name. + if blkname in special_names[name]: + return self._make_block(name, x, y, defaults) + # Check for a macro + if blkname in MACROS: + self.new_macro(blkname, x, y) + return 0 # Fix me: calculate flow position + # Block not found + raise logoerror("#syntaxerror") + return -1 + + def prim_load_palette(self, arg): + ''' Select a palette ''' + if type(arg) in [int, float]: + if int(arg) < 0 or int(arg) > len(palette_names): + raise logoerror("#syntaxerror") + else: + self.show_toolbar_palette(int(arg)) + else: + if type(arg) == unicode: + arg = arg.encode('utf-8') + if arg in palette_names or arg in palette_i18n_names: + self.show_toolbar_palette(palette_name_to_index(arg)) + else: + raise logoerror("#syntaxerror") diff --git a/TurtleArtActivity.py b/TurtleArtActivity.py index 2fc7283..3cbe1a5 100644 --- a/TurtleArtActivity.py +++ b/TurtleArtActivity.py @@ -28,7 +28,6 @@ import gtk import cairo import gobject import dbus -import glob import logging _logger = logging.getLogger('turtleart-activity') @@ -42,14 +41,17 @@ except ImportError: HAS_TOOLBARBOX = False from sugar.graphics.toolbutton import ToolButton from sugar.graphics.radiotoolbutton import RadioToolButton -from sugar.graphics.alert import (ConfirmationAlert, NotifyAlert) +from sugar.graphics.alert import (ConfirmationAlert, Alert) from sugar.graphics import style from sugar.graphics.objectchooser import ObjectChooser -from sugar import mime +from sugar.graphics.icon import Icon +from sugar.graphics.xocolor import XoColor from sugar.datastore import datastore from sugar import profile +from sugar import mime import os +import glob import tarfile import subprocess import ConfigParser @@ -63,15 +65,19 @@ except ImportError: from gettext import gettext as _ +from TurtleArt.taplugin import (load_a_plugin, cancel_plugin_install, + complete_plugin_install) from TurtleArt.tapalette import (palette_names, help_strings, help_palettes, - help_windows) + help_windows, default_values) from TurtleArt.taconstants import (BLOCK_SCALE, XO1, XO15, XO175, XO4, MIMETYPE) from TurtleArt.taexportlogo import save_logo +from TurtleArt.taexportpython import save_python from TurtleArt.tautils import (data_to_file, data_to_string, data_from_string, get_path, chooser_dialog, get_hardware) from TurtleArt.tawindow import TurtleArtWindow from TurtleArt.tacollaboration import Collaboration +from TurtleArt.taprimitive import PyExportError if HAS_TOOLBARBOX: from util.helpbutton import (HelpButton, add_section, add_paragraph) @@ -80,6 +86,8 @@ if HAS_TOOLBARBOX: class TurtleArtActivity(activity.Activity): ''' Activity subclass for Turtle Art ''' _HOVER_HELP = '/desktop/sugar/activities/turtleart/hoverhelp' + _ORIENTATION = '/desktop/sugar/activities/turtleart/orientation' + _COORDINATE_SCALE = '/desktop/sugar/activities/turtleart/coordinatescale' def __init__(self, handle): ''' Set up the toolbars, canvas, sharing, etc. ''' @@ -90,8 +98,13 @@ class TurtleArtActivity(activity.Activity): self.tw = None self.init_complete = False + self._stop_help = False + self.bundle_path = activity.get_bundle_path() + + self.error_list = [] + self.palette_buttons = [] self._palette_names = [] self._overflow_buttons = [] @@ -107,18 +120,28 @@ class TurtleArtActivity(activity.Activity): self._setup_toolbar() _logger.debug('_setup_canvas') - self._canvas = self._setup_canvas(self._setup_scrolled_window()) - - # FIX ME: not sure how or why self.canvas gets overwritten - # It is set to self.sw in _setup_canvas but None here. - # We need self.canvas for generating the preview image - self.canvas = self.sw + self._setup_canvas(self._setup_scrolled_window()) _logger.debug('_setup_palette_toolbar') self._setup_palette_toolbar() self._setup_extra_controls() _logger.debug('_setup_sharing') + if self.shared_activity: + # We're joining + if not self.get_shared(): + xocolors = XoColor(profile.get_color().to_string()) + share_icon = Icon(icon_name='zoom-neighborhood', + xo_color=xocolors) + self._joined_alert = Alert() + self._joined_alert.props.icon = share_icon + self._joined_alert.props.title = _('Please wait') + self._joined_alert.props.msg = _('Starting connection...') + self.add_alert(self._joined_alert) + + # Wait for joined signal + self.connect("joined", self._joined_cb) + self._setup_sharing() # Activity count is the number of times this instance has been @@ -137,6 +160,16 @@ class TurtleArtActivity(activity.Activity): self.client = gconf.client_get_default() if self.client.get_int(self._HOVER_HELP) == 1: self._do_hover_help_toggle(None) + if not self.client.get_int(self._COORDINATE_SCALE) in [0, 1]: + self.tw.coord_scale = 1 + self.do_rescale_cb(None) + else: + self.tw.coord_scale = 0 + self.do_rescale_cb(None) + + self._selected_sample = None + self._sample_window = None + self.init_complete = True if not hasattr(self, '_offsets'): @@ -150,6 +183,30 @@ class TurtleArtActivity(activity.Activity): self._challenge_window = None self._load_level() + def update_palette_from_metadata(self): + if HAS_GCONF: + # We have to wait to set the orientation for the palettes + # to be loaded. + self.client = gconf.client_get_default() + if self.client.get_int(self._ORIENTATION) == 1: + self.tw.set_orientation(1) + + if 'palette' in self.metadata: + n = int(self.metadata['palette']) + if n == -1: + self.tw.hideshow_palette(False) + else: + # Try to set radio button to active + if n < len(self.palette_buttons): + self.palette_buttons[n].set_active(True) + else: + self.tw.show_palette(n=0) + if 'orientation' in self.metadata: + self.tw.set_orientation(int(self.metadata['orientation'])) + else: + # Else start on the Turtle palette + self.tw.show_palette(n=0) + def check_buttons_for_fit(self): ''' Check to see which set of buttons to display ''' if not self.has_toolbarbox: @@ -159,51 +216,96 @@ class TurtleArtActivity(activity.Activity): # scrolling window self._setup_palette_toolbar() - if self.samples_button in self._toolbox.toolbar: - self._toolbox.toolbar.remove(self.extras_separator) - self._toolbox.toolbar.remove(self.samples_button) - self._toolbox.toolbar.remove(self.stop_separator) - self._toolbox.toolbar.remove(self.stop_button) + if self.samples_button in self.toolbox.toolbar: + self.toolbox.toolbar.remove(self.extras_separator) + self.toolbox.toolbar.remove(self.samples_button) + self.toolbox.toolbar.remove(self.stop_separator) + self.toolbox.toolbar.remove(self.stop_button) self._view_toolbar.remove(self._coordinates_toolitem) if gtk.gdk.screen_width() / 14 < style.GRID_CELL_SIZE: self.samples_button2.show() self.samples_label2.show() - self._toolbox.toolbar.insert(self.stop_button, -1) + self.toolbox.toolbar.insert(self.stop_button, -1) else: self.samples_button2.hide() self.samples_label2.hide() - self._toolbox.toolbar.insert(self.extras_separator, -1) + self.toolbox.toolbar.insert(self.extras_separator, -1) self.extras_separator.props.draw = True self.extras_separator.show() - self._toolbox.toolbar.insert(self.samples_button, -1) + self.toolbox.toolbar.insert(self.samples_button, -1) self.samples_button.show() - self._toolbox.toolbar.insert(self.stop_separator, -1) + self.toolbox.toolbar.insert(self.stop_separator, -1) self.stop_separator.show() - self._toolbox.toolbar.insert(self.stop_button, -1) + self.toolbox.toolbar.insert(self.stop_button, -1) self._view_toolbar.insert(self._coordinates_toolitem, -1) - self._toolbox.show_all() + self.toolbox.show_all() # Activity toolbar callbacks def do_save_as_logo_cb(self, button): ''' Write UCB logo code to datastore. ''' self.save_as_logo.set_icon('logo-saveon') + if hasattr(self, 'get_window'): + if hasattr(self.get_window(), 'get_cursor'): + self._old_cursor = self.get_window().get_cursor() + self.get_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + gobject.timeout_add(250, self.__save_as_logo) + + def __save_as_logo(self): logo_code_path = self._dump_logo_code() - if logo_code_path is None: - return + if logo_code_path is not None: + dsobject = datastore.create() + dsobject.metadata['title'] = self.metadata['title'] + '.lg' + dsobject.metadata['mime_type'] = 'text/plain' + dsobject.metadata['icon-color'] = profile.get_color().to_string() + dsobject.set_file_path(logo_code_path) + datastore.write(dsobject) + dsobject.destroy() + os.remove(logo_code_path) + self.save_as_logo.set_icon('logo-saveoff') + if hasattr(self, 'get_window'): + self.get_window().set_cursor(self._old_cursor) - dsobject = datastore.create() - dsobject.metadata['title'] = self.metadata['title'] + '.lg' - dsobject.metadata['mime_type'] = 'text/plain' - dsobject.metadata['icon-color'] = profile.get_color().to_string() - dsobject.set_file_path(logo_code_path) - datastore.write(dsobject) - dsobject.destroy() - os.remove(logo_code_path) - gobject.timeout_add(250, self.save_as_logo.set_icon, 'logo-saveoff') - self._notify_successful_save(title=_('Save as Logo')) + def do_save_as_python_cb(self, widget): + ''' Callback for saving the project as Python code. ''' + self.save_as_python.set_icon('python-saveon') + if hasattr(self, 'get_window'): + if hasattr(self.get_window(), 'get_cursor'): + self._old_cursor = self.get_window().get_cursor() + self.get_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + gobject.timeout_add(250, self.__save_as_python) + + def __save_as_python(self): + # catch PyExportError and display a user-friendly message instead + try: + pythoncode = save_python(self.tw) + except PyExportError as pyee: + if pyee.block is not None: + pyee.block.highlight() + self.tw.showlabel('status', str(pyee)) + _logger.debug(pyee) + + if pythoncode: + datapath = get_path(activity, 'instance') + python_code_path = os.path.join(datapath, 'tmpfile.py') + f = file(python_code_path, 'w') + f.write(pythoncode) + f.close() + + dsobject = datastore.create() + dsobject.metadata['title'] = self.metadata['title'] + '.py' + dsobject.metadata['mime_type'] = 'text/x-python' + dsobject.metadata['icon-color'] = profile.get_color().to_string() + dsobject.set_file_path(python_code_path) + datastore.write(dsobject) + dsobject.destroy() + + os.remove(python_code_path) + self.save_as_python.set_icon('python-saveoff') + if hasattr(self, 'get_window'): + self.get_window().set_cursor(self._old_cursor) def do_load_ta_project_cb(self, button, new=False): ''' Load a project from the Journal. ''' @@ -230,7 +332,6 @@ class TurtleArtActivity(activity.Activity): def do_load_ta_plugin_cb(self, button): ''' Load a plugin from the Journal. ''' - # While the file is loading, use the watch cursor if hasattr(self, 'get_window'): if hasattr(self.get_window(), 'get_cursor'): self._old_cursor = self.get_window().get_cursor() @@ -257,13 +358,27 @@ class TurtleArtActivity(activity.Activity): ''' Save the canvas to the Journal. ''' self.save_as_image.set_icon('image-saveon') _logger.debug('saving image to journal') + if hasattr(self, 'get_window'): + if hasattr(self.get_window(), 'get_cursor'): + self._old_cursor = self.get_window().get_cursor() + self.get_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + gobject.timeout_add(250, self.__save_as_image) + def __save_as_image(self): self.tw.save_as_image() - gobject.timeout_add(250, self.save_as_image.set_icon, 'image-saveoff') - self._notify_successful_save(title=_('Save as image')) + self.save_as_image.set_icon('image-saveoff') + if hasattr(self, 'get_window'): + self.get_window().set_cursor(self._old_cursor) def do_keep_cb(self, button): ''' Save a snapshot of the project to the Journal. ''' + if hasattr(self, 'get_window'): + if hasattr(self.get_window(), 'get_cursor'): + self._old_cursor = self.get_window().get_cursor() + self.get_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + gobject.timeout_add(250, self.__keep) + + def __keep(self): tmpfile = self._dump_ta_code() if tmpfile is not None: dsobject = datastore.create() @@ -276,7 +391,9 @@ class TurtleArtActivity(activity.Activity): datastore.write(dsobject) dsobject.destroy() os.remove(tmpfile) - self._notify_successful_save(title=_('Save snapshot')) + + if hasattr(self, 'get_window'): + self.get_window().set_cursor(self._old_cursor) # Main/palette toolbar button callbacks @@ -285,7 +402,7 @@ class TurtleArtActivity(activity.Activity): if self.tw.palette: self.tw.hideshow_palette(False) self.do_hidepalette() - if self.has_toolbarbox and self.tw.selected_palette is not None: + if not self.has_toolbarbox and self.tw.selected_palette is not None: self.palette_buttons[self.tw.selected_palette].set_icon( palette_names[self.tw.selected_palette] + 'off') else: @@ -324,7 +441,8 @@ class TurtleArtActivity(activity.Activity): self.tw.no_help = False self._hover_help_toggle.set_icon('help-off') self._hover_help_toggle.set_tooltip(_('Turn off hover help')) - self.client.set_int(self._HOVER_HELP, 0) + if HAS_GCONF: + self.client.set_int(self._HOVER_HELP, 0) else: self.tw.no_help = True self.tw.last_label = None @@ -332,7 +450,8 @@ class TurtleArtActivity(activity.Activity): self.tw.status_spr.hide() self._hover_help_toggle.set_icon('help-on') self._hover_help_toggle.set_tooltip(_('Turn on hover help')) - self.client.set_int(self._HOVER_HELP, 1) + if HAS_GCONF: + self.client.set_int(self._HOVER_HELP, 1) # These methods are called both from toolbar buttons and blocks. @@ -503,7 +622,7 @@ class TurtleArtActivity(activity.Activity): if self.tw.block_scale in BLOCK_SCALE: i = BLOCK_SCALE.index(self.tw.block_scale) + inc else: - i = BLOCK_SCALE[3] # 2.0 + i = 3 if i < 0: self.tw.block_scale = BLOCK_SCALE[0] elif i == len(BLOCK_SCALE): @@ -534,18 +653,30 @@ class TurtleArtActivity(activity.Activity): self.tw.set_metric(True) def do_rescale_cb(self, button): - ''' Rescale coordinate system (100==height/2 or 100 pixels). ''' + ''' Rescale coordinate system (20==height/2 or 100 pixels). ''' if self.tw.coord_scale == 1: - self.tw.coord_scale = self.tw.height / 200 + self.tw.coord_scale = self.tw.height / 40 self.rescale_button.set_icon('contract-coordinates') self.rescale_button.set_tooltip(_('Rescale coordinates down')) + default_values['forward'] = [10] + default_values['back'] = [10] + default_values['arc'] = [90, 10] + default_values['setpensize'] = [1] + self.tw.turtles.get_active_turtle().set_pen_size(1) else: self.tw.coord_scale = 1 self.rescale_button.set_icon('expand-coordinates') self.rescale_button.set_tooltip(_('Rescale coordinates up')) - self.tw.eraser_button() + default_values['forward'] = [100] + default_values['back'] = [100] + default_values['arc'] = [90, 100] + default_values['setpensize'] = [5] + self.tw.turtles.get_active_turtle().set_pen_size(5) + if HAS_GCONF: + self.client.set_int(self._COORDINATE_SCALE, self.tw.coord_scale) # Given the change in how overlays are handled (v123), there is no way # to erase and then redraw the overlays. + self.tw.eraser_button() def _do_help_cb(self, button): if self._selected_challenge is None: @@ -637,8 +768,10 @@ class TurtleArtActivity(activity.Activity): def _setup_toolbar(self): ''' Setup toolbar according to Sugar version. ''' if self.has_toolbarbox: + self.max_participants = 4 + self._setup_toolbar_help() - self._toolbox = ToolbarBox() + self.toolbox = ToolbarBox() self.activity_toolbar_button = ActivityToolbarButton(self) @@ -659,33 +792,31 @@ class TurtleArtActivity(activity.Activity): _('Help'), self._do_help_cb, None) - # self._help_button = HelpButton(self) self._make_load_save_buttons(self.activity_toolbar_button) self.activity_toolbar_button.show() - self._toolbox.toolbar.insert(self.activity_toolbar_button, -1) + self.toolbox.toolbar.insert(self.activity_toolbar_button, -1) self.edit_toolbar_button.show() - self._toolbox.toolbar.insert(self.edit_toolbar_button, -1) + self.toolbox.toolbar.insert(self.edit_toolbar_button, -1) self.view_toolbar_button.show() - self._toolbox.toolbar.insert(self.view_toolbar_button, -1) + self.toolbox.toolbar.insert(self.view_toolbar_button, -1) self.palette_toolbar_button.show() - self._toolbox.toolbar.insert(self.palette_toolbar_button, -1) + self.toolbox.toolbar.insert(self.palette_toolbar_button, -1) - self.set_toolbar_box(self._toolbox) - self.palette_toolbar_button.set_expanded(True) + self.set_toolbar_box(self.toolbox) else: - self._toolbox = activity.ActivityToolbox(self) - self.set_toolbox(self._toolbox) + self.toolbox = activity.ActivityToolbox(self) + self.set_toolbox(self.toolbox) self._project_toolbar = gtk.Toolbar() - self._toolbox.add_toolbar(_('Project'), self._project_toolbar) + self.toolbox.add_toolbar(_('Project'), self._project_toolbar) self._view_toolbar = gtk.Toolbar() - self._toolbox.add_toolbar(_('View'), self._view_toolbar) + self.toolbox.add_toolbar(_('View'), self._view_toolbar) edit_toolbar = gtk.Toolbar() - self._toolbox.add_toolbar(_('Edit'), edit_toolbar) + self.toolbox.add_toolbar(_('Edit'), edit_toolbar) journal_toolbar = gtk.Toolbar() - self._toolbox.add_toolbar(_('Save/Load'), journal_toolbar) + self.toolbox.add_toolbar(_('Save/Load'), journal_toolbar) self._make_palette_buttons(self._project_toolbar, palette_button=True) @@ -737,10 +868,14 @@ class TurtleArtActivity(activity.Activity): edit_toolbar.show() self._view_toolbar.show() - self._toolbox.show() + self.toolbox.show() - if not self.has_toolbarbox: - self._toolbox.set_current_toolbar(1) + if self.has_toolbarbox: + self.edit_toolbar_button.set_expanded(True) + self.edit_toolbar_button.set_expanded(False) + self.palette_toolbar_button.set_expanded(True) + else: + self.toolbox.set_current_toolbar(1) def _setup_extra_controls(self): ''' Add the rest of the buttons to the main toolbar ''' @@ -753,24 +888,24 @@ class TurtleArtActivity(activity.Activity): self._make_project_buttons(self._project_toolbar) return - self._make_project_buttons(self._toolbox.toolbar) + self._make_project_buttons(self.toolbox.toolbar) self.extras_separator = self._add_separator( - self._toolbox.toolbar, expand=False, visible=True) + self.toolbox.toolbar, expand=False, visible=True) self.samples_button = self._add_button( 'ta-open', _('Load challenges'), self._create_store, - self._toolbox.toolbar) + self.toolbox.toolbar) - self._toolbox.toolbar.insert(self._help_button, -1) + self.toolbox.toolbar.insert(self._help_button, -1) self._help_button.show() self.stop_separator = self._add_separator( - self._toolbox.toolbar, expand=True, visible=False) + self.toolbox.toolbar, expand=True, visible=False) self.stop_button = StopButton(self) self.stop_button.props.accelerator = 'Q' - self._toolbox.toolbar.insert(self.stop_button, -1) + self.toolbox.toolbar.insert(self.stop_button, -1) self.stop_button.show() def _setup_toolbar_help(self): @@ -816,12 +951,11 @@ class TurtleArtActivity(activity.Activity): help_palettes['activity-toolbar'].show() add_paragraph(help_box, _('Share selected blocks'), icon='shareon') - if gtk.gdk.screen_width() < 1024: - add_paragraph(help_box, _('Save/Load'), icon='save-load') - else: - add_section(help_box, _('Save/Load'), icon='turtleoff') + add_paragraph(help_box, _('Save/Load'), icon='save-load') add_paragraph(help_box, _('Save as image'), icon='image-saveoff') add_paragraph(help_box, _('Save as Logo'), icon='logo-saveoff') + add_paragraph(help_box, _('Save as Python'), icon='python-saveoff') + add_paragraph(help_box, _('Save snapshot'), icon='filesaveoff') add_paragraph(help_box, _('Load project'), icon='load-from-journal') home = os.environ['HOME'] if activity.get_bundle_path()[0:len(home)] == home: @@ -999,10 +1133,10 @@ class TurtleArtActivity(activity.Activity): self._share_cb, toolbar) if self.has_toolbarbox: self._add_separator(toolbar, expand=False, visible=True) - save_load_button = self._add_button( - 'save-load', _('Save/Load'), self._save_load_palette_cb, + save_button = self._add_button( + 'save', _('Save'), self._save_load_palette_cb, toolbar) - self._palette = save_load_button.get_palette() + self._save_palette = save_button.get_palette() button_box = gtk.VBox() self.save_as_image, label = self._add_button_and_label( 'image-saveoff', _('Save as image'), self.do_save_as_image_cb, @@ -1010,16 +1144,28 @@ class TurtleArtActivity(activity.Activity): self.save_as_logo, label = self._add_button_and_label( 'logo-saveoff', _('Save as Logo'), self.do_save_as_logo_cb, None, button_box) - - # When screen is in portrait mode, the buttons don't fit - # on the main toolbar, so put them here. + self.save_as_python, label = self._add_button_and_label( + 'python-saveoff', _('Save as Python'), + self.do_save_as_python_cb, + None, button_box) self.keep_button2, self.keep_label2 = self._add_button_and_label( 'filesaveoff', _('Save snapshot'), self.do_keep_cb, None, button_box) + + load_button = self._add_button( + 'load', _('Load'), self._save_load_palette_cb, + toolbar) + button_box.show_all() + self._save_palette.set_content(button_box) + + self._load_palette = load_button.get_palette() + button_box = gtk.VBox() + # When screen is in portrait mode, the buttons don't fit + # on the main toolbar, so put them here. self.samples_button2, self.samples_label2 = \ self._add_button_and_label('ta-open', - _('Load challenges'), - self._create_store, + _('Load example'), + self.do_samples_cb, None, button_box) @@ -1038,7 +1184,7 @@ class TurtleArtActivity(activity.Activity): 'pippy-openoff', _('Load Python block'), self.do_load_python_cb, None, button_box) button_box.show_all() - self._palette.set_content(button_box) + self._load_palette.set_content(button_box) else: self.save_as_image = self._add_button( 'image-saveoff', _('Save as image'), self.do_save_as_image_cb, @@ -1046,10 +1192,14 @@ class TurtleArtActivity(activity.Activity): self.save_as_logo = self._add_button( 'logo-saveoff', _('Save as Logo'), self.do_save_as_logo_cb, toolbar) + self.save_as_python = self._add_button( + 'python-saveoff', _('Save as Python'), + self.do_save_as_python_cb, + toolbar) self.keep_button = self._add_button( 'filesaveoff', _('Save snapshot'), self.do_keep_cb, toolbar) self.load_ta_project = self._add_button( - 'load-from-journal', _('Load project'), + 'load-from-journal', _('Add project'), self.do_load_ta_project_cb, toolbar) # Only enable plugin loading if installed in $HOME if activity.get_bundle_path()[0:len(home)] == home: @@ -1061,13 +1211,12 @@ class TurtleArtActivity(activity.Activity): self.do_load_python_cb, toolbar) def _save_load_palette_cb(self, button): - if self._palette: - if not self._palette.is_up(): - self._palette.popup(immediate=True, - state=self._palette.SECONDARY) + palette = button.get_palette() + if palette: + if not palette.is_up(): + palette.popup(immediate=True, state=palette.SECONDARY) else: - self._palette.popdown(immediate=True) - return + palette.popdown(immediate=True) def _make_palette_buttons(self, toolbar, palette_button=False): ''' Creates the palette and block buttons for both toolbar types''' @@ -1208,6 +1357,12 @@ class TurtleArtActivity(activity.Activity): self._collaboration = Collaboration(self.tw, self) self._collaboration.setup() + def _joined_cb(self, widget): + if self._joined_alert is not None: + self.remove_alert(self._joined_alert) + self._joined_alert = None + self.set_canvas(self.fixed) + def send_xy(self): ''' Resync xy position (and orientation) of my turtle. ''' self._collaboration.send_my_xy() @@ -1243,112 +1398,26 @@ class TurtleArtActivity(activity.Activity): self._offsets[basename][2]) self.metadata['mime_type'] = MIMETYPE[0] self.metadata['turtle blocks'] = ''.join(self.tw.used_block_list) - self.metadata['public'] = data_to_string(['activity count', - 'turtle blocks']) - _logger.debug('Wrote to file: %s' % (file_path)) - - def _load_a_plugin(self, tmp_dir): - ''' Load a plugin from the Journal and initialize it ''' - plugin_path = os.path.join(tmp_dir, 'plugin.info') - _logger.debug(plugin_path) - file_info = ConfigParser.ConfigParser() - if len(file_info.read(plugin_path)) == 0: - _logger.debug('Required file plugin.info could not be found.') - self.tw.showlabel('status', - label=_('Plugin could not be installed.')) - elif not file_info.has_option('Plugin', 'name'): - _logger.debug('Required open name not found in \ -Plugin section of plugin.info file.') - self.tw.showlabel( - 'status', label=_('Plugin could not be installed.')) - else: - plugin_name = file_info.get('Plugin', 'name') - _logger.debug('Plugin name: %s' % (plugin_name)) - tmp_path = os.path.join(tmp_dir, plugin_name) - plugin_path = os.path.join(activity.get_bundle_path(), 'plugins') - if os.path.exists(os.path.join(plugin_path, plugin_name)): - self._reload_plugin_alert(tmp_dir, tmp_path, plugin_path, - plugin_name, file_info) - else: - self._complete_plugin_install(tmp_dir, tmp_path, plugin_path, - plugin_name, file_info) - - def _complete_plugin_install(self, tmp_dir, tmp_path, plugin_path, - plugin_name, file_info): - ''' We complete the installation directly or from ConfirmationAlert ''' - status = subprocess.call(['cp', '-r', tmp_path, plugin_path + '/']) - if status == 0: - # Save the plugin.info file in the plugin directory - subprocess.call(['cp', os.path.join(tmp_dir, 'plugin.info'), - os.path.join(plugin_path, plugin_name) + '/']) - _logger.debug('Plugin installed successfully.') - if self.has_toolbarbox: - palette_name_list = [] - if file_info.has_option('Plugin', 'palette'): - palette_name_list = file_info.get( - 'Plugin', 'palette').split(',') - create_palette = [] - for palette_name in palette_name_list: - if not palette_name.strip() in palette_names: - create_palette.append(True) - else: - create_palette.append(False) - _logger.debug('Initializing plugin...') - self.tw.init_plugin(plugin_name) - self.tw.turtleart_plugins[-1].setup() - self.tw.load_media_shapes() - for i, palette_name in enumerate(palette_name_list): - if create_palette[i]: - _logger.debug('Creating plugin palette %s (%d)' % - (palette_name.strip(), i)) - j = len(self.palette_buttons) - self.palette_buttons.append( - self._radio_button_factory( - palette_name.strip() + 'off', - self._palette_toolbar, - self.do_palette_buttons_cb, - j - 1, - help_strings[palette_name.strip()], - self.palette_buttons[0])) - self._overflow_buttons.append( - self._add_button( - palette_name.strip() + 'off', - None, - self.do_palette_buttons_cb, - None, - arg=j - 1)) - self._overflow_box.pack_start( - self._overflow_buttons[j - 1]) - self.tw.palettes.insert(j - 1, []) - self.tw.palette_sprs.insert(j - 1, [None, None]) - else: - _logger.debug('Palette already exists... \ -skipping insert') - # We need to change the index associated with the - # Trash Palette Button. - j = len(palette_names) - pidx = palette_names.index(palette_name.strip()) - self.palette_buttons[pidx].connect( - 'clicked', self.do_palette_buttons_cb, j - 1) - self._overflow_buttons[pidx].connect( - 'clicked', self.do_palette_buttons_cb, j - 1) - _logger.debug('reinitializing palette toolbar') - self._setup_palette_toolbar() - else: - self.tw.showlabel('status', - label=_('Please restart Turtle Art \ -in order to use the plugin.')) + # Deprecated + # self.metadata['public'] = data_to_string(['activity count', + # 'turtle blocks']) + if self.tw.palette: + self.metadata['palette'] = str(self.tw.selected_palette) else: - self.tw.showlabel( - 'status', label=_('Plugin could not be installed.')) - status = subprocess.call(['rm', '-r', tmp_path]) - if status != 0: - _logger.debug('Problems cleaning up tmp_path.') - shutil.rmtree(tmp_dir) + self.metadata['palette'] = '-1' + self.metadata['orientation'] = str(self.tw.orientation) + if HAS_GCONF: + self.client.set_int(self._ORIENTATION, self.tw.orientation) + if len(self.error_list) > 0: + errors = [] + if 'error_list' in self.metadata: + for error in data_from_string(self.metadata['error_list']): + errors.append(error) + for error in self.error_list: + errors.append(error) + self.metadata['error_list'] = data_to_string(errors) + _logger.debug('Wrote to file: %s' % (file_path)) - def _cancel_plugin_install(self, tmp_dir): - ''' If we cancel, just cleanup ''' - shutil.rmtree(tmp_dir) def _reload_plugin_alert(self, tmp_dir, tmp_path, plugin_path, plugin_name, file_info): @@ -1363,8 +1432,8 @@ in order to use the plugin.')) if response_id is gtk.RESPONSE_OK: _logger.debug('continue to install') self.remove_alert(alert) - self._complete_plugin_install(tmp_dir, tmp_path, plugin_path, - plugin_name, file_info) + complete_plugin_install(self, tmp_dir, tmp_path, plugin_path, + plugin_name, file_info) elif response_id is gtk.RESPONSE_CANCEL: _logger.debug('cancel install') self.remove_alert(alert) @@ -1424,7 +1493,7 @@ in order to use the plugin.')) gobject.idle_add(self._project_loader, turtle_code) else: _logger.debug('load a plugin from %s' % (tmp_dir)) - self._load_a_plugin(tmp_dir) + load_a_plugin(self, tmp_dir) self.restore_cursor() except: _logger.debug('Could not extract files from %s.' % @@ -1557,6 +1626,25 @@ in order to use the plugin.')) self._old_cursor = self.get_window().get_cursor() self.get_window().set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND1)) + def empty_trash_alert(self, title, msg): + ''' We get confirmation from the user before emptying the trash ''' + alert = ConfirmationAlert() + alert.props.title = title + alert.props.msg = msg + + def _empty_trash_alert_response_cb(alert, response_id, self): + if response_id is gtk.RESPONSE_OK: + _logger.debug('emptying the trash') + self.remove_alert(alert) + self.tw._empty_trash() + elif response_id is gtk.RESPONSE_CANCEL: + _logger.debug('cancel emptying the trash') + self.remove_alert(alert) + + alert.connect('response', _empty_trash_alert_response_cb, self) + self.add_alert(alert) + alert.show() + def _add_label(self, string, toolbar, width=None): ''' Add a label to a toolbar. ''' label = gtk.Label(string) @@ -1609,6 +1697,7 @@ in order to use the plugin.')) return button def _load_level(self, custom=False): + logging.error('LOAD LEVEL') self.tw.canvas.clearscreen() self._draw_cartoon() if custom: @@ -1673,19 +1762,6 @@ in order to use the plugin.')) button_and_label.show() return button, label - def _notify_successful_save(self, title='', msg=''): - ''' Notify user when saves are completed ''' - - def _notification_alert_response_cb(alert, response_id, self): - self.remove_alert(alert) - - alert = NotifyAlert() - alert.props.title = title - alert.connect('response', _notification_alert_response_cb, self) - alert.props.msg = msg - self.add_alert(alert) - alert.show() - def hide_store(self, widget=None): if self._challenge_window is not None: self._challenge_box.hide() @@ -1773,6 +1849,17 @@ in order to use the plugin.')) filepath, 100, 100) store.append([pixbuf, filepath]) + def is_toolbar_expanded(self): + if self.palette_toolbar_button.is_expanded(): + return True + elif self.edit_toolbar_button.is_expanded(): + return True + elif self.view_toolbar_button.is_expanded(): + return True + elif self.activity_toolbar_button.is_expanded(): + return True + return False + def _scan_for_challenges(self): file_list = list(glob.glob(os.path.join(activity.get_bundle_path(), 'samples', 'thumbnails', diff --git a/icons/load.svg b/icons/load.svg new file mode 100644 index 0000000..189948f --- /dev/null +++ b/icons/load.svg @@ -0,0 +1,137 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/python-saveoff.svg b/icons/python-saveoff.svg new file mode 100644 index 0000000..1064219 --- /dev/null +++ b/icons/python-saveoff.svg @@ -0,0 +1,111 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/icons/python-saveon.svg b/icons/python-saveon.svg new file mode 100644 index 0000000..8871ae7 --- /dev/null +++ b/icons/python-saveon.svg @@ -0,0 +1,111 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/icons/save.svg b/icons/save.svg new file mode 100644 index 0000000..1f1ad11 --- /dev/null +++ b/icons/save.svg @@ -0,0 +1,137 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/Cartesian.svg b/images/Cartesian.svg index 7a29e22..b34751f 100644 --- a/images/Cartesian.svg +++ b/images/Cartesian.svg @@ -1,211 +1,213 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -25 + -20 + -15 + -10 + -5 + 5 + 10 + 0 + 15 + 20 + 25 + -15 + -10 + -5 + 5 + 10 + 15 diff --git a/images/dupstack.svg b/images/dupstack.svg index 6a0f3fd..f3eacd9 100644 --- a/images/dupstack.svg +++ b/images/dupstack.svg @@ -47,21 +47,6 @@ style="font-size:12px">X - - - x - @@ -91,8 +76,54 @@ id="tspan2488" style="font-size:24px">! - + + + + + + + + x + + 1 + 2 diff --git a/images/noconnection.svg b/images/noconnection.svg new file mode 100644 index 0000000..e1f54ed --- /dev/null +++ b/images/noconnection.svg @@ -0,0 +1,122 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + X + + + + + + + + + ! + + + + + + + + + + + + + + + diff --git a/plugins/accelerometer/accelerometer.py b/plugins/accelerometer/accelerometer.py new file mode 100644 index 0000000..b792f5e --- /dev/null +++ b/plugins/accelerometer/accelerometer.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +#Copyright (c) 2011 Walter Bender +# +# 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 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os + +from gettext import gettext as _ + +from plugins.plugin import Plugin + +from TurtleArt.tapalette import make_palette +from TurtleArt.tautils import debug_output +from TurtleArt.taprimitive import Primitive + +import logging +_logger = logging.getLogger('turtleart-activity accelerometer plugin') + + +ACCELEROMETER_DEVICE = '/sys/devices/platform/lis3lv02d/position' + + +class Accelerometer(Plugin): + + def __init__(self, parent): + Plugin.__init__(self) + self._parent = parent + if os.path.exists(ACCELEROMETER_DEVICE): + self._status = True + else: + self._status = False + self.running_sugar = self._parent.running_sugar + + def setup(self): + # set up accelerometer specific blocks + palette = make_palette('extras', + colors=["#FF0000", "#A00000"], + help_string=_('Palette of extra options'), + position=8, + translation=_('extras')) + ''' + palette = make_palette('sensor', + colors=["#FF6060", "#A06060"], + help_string=_('Palette of sensor blocks'), + position=6) + ''' + + if self._status: + palette.add_block('xyz', + hidden=True, + style='basic-style-extended-vertical', + label=_('acceleration'), + help_string=\ + _('push acceleration in x, y, z to heap'), + prim_name='xyz') + else: + palette.add_block('xyz', + hidden=True, + style='basic-style-extended-vertical', + label=_('acceleration'), + help_string=\ + _('push acceleration in x, y, z to heap'), + prim_name='xyz') + + self._parent.lc.def_prim( + 'xyz', 0, + Primitive(self.prim_xyz)) + + def _status_report(self): + debug_output('Reporting accelerator status: %s' % (str(self._status))) + return self._status + + # Block primitives used in talogo + + def prim_xyz(self): + ''' push accelerometer xyz to stack ''' + if not self._status: + self._parent.lc.heap.append(0) + self._parent.lc.heap.append(0) + self._parent.lc.heap.append(0) + else: + fh = open(ACCELEROMETER_DEVICE) + string = fh.read() + xyz = string[1:-2].split(',') + self._parent.lc.heap.append(float(xyz[2]) / 18) + self._parent.lc.heap.append(float(xyz[1]) / 18) + self._parent.lc.heap.append(float(xyz[0]) / 18) + fh.close() diff --git a/plugins/accelerometer/icons/extrasoff.svg b/plugins/accelerometer/icons/extrasoff.svg new file mode 100644 index 0000000..a956f03 --- /dev/null +++ b/plugins/accelerometer/icons/extrasoff.svg @@ -0,0 +1,75 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/plugins/accelerometer/icons/extrason.svg b/plugins/accelerometer/icons/extrason.svg new file mode 100644 index 0000000..7ee08bf --- /dev/null +++ b/plugins/accelerometer/icons/extrason.svg @@ -0,0 +1,158 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/accelerometer/icons/sensoroff.svg b/plugins/accelerometer/icons/sensoroff.svg new file mode 100644 index 0000000..0a16670 --- /dev/null +++ b/plugins/accelerometer/icons/sensoroff.svg @@ -0,0 +1,79 @@ + + + +image/svg+xml + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/accelerometer/icons/sensoron.svg b/plugins/accelerometer/icons/sensoron.svg new file mode 100644 index 0000000..d756860 --- /dev/null +++ b/plugins/accelerometer/icons/sensoron.svg @@ -0,0 +1,63 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/plugins/audio_sensors/audio_sensors.py b/plugins/audio_sensors/audio_sensors.py new file mode 100644 index 0000000..027fc67 --- /dev/null +++ b/plugins/audio_sensors/audio_sensors.py @@ -0,0 +1,483 @@ +#!/usr/bin/env python +#Copyright (c) 2011, 2012 Walter Bender +# +# 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 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ + +try: + from numpy.fft import rfft + PITCH_AVAILABLE = True +except: + PITCH_AVAILABLE = False + +from plugins.plugin import Plugin + +from plugins.audio_sensors.audiograb import (AudioGrab, + SENSOR_DC_NO_BIAS, SENSOR_DC_BIAS, SENSOR_AC_BIAS) + +from plugins.audio_sensors.ringbuffer import RingBuffer1d + +from TurtleArt.tapalette import make_palette +from TurtleArt.taconstants import XO1, XO15, XO175, XO30, XO4 +from TurtleArt.tautils import debug_output +from TurtleArt.taprimitive import (ConstantArg, Primitive) +from TurtleArt.tatype import TYPE_NUMBER + +import logging +_logger = logging.getLogger('turtleart-activity audio sensors plugin') + + +def _avg(array, abs_value=False): + ''' Calc. the average value of an array ''' + if len(array) == 0: + return 0 + array_sum = 0 + if abs_value: + for a in array: + array_sum += abs(a) + else: + for a in array: + array_sum += a + return float(array_sum) / len(array) + + +class Audio_sensors(Plugin): + + def __init__(self, parent): + Plugin.__init__(self) + self._parent = parent + self.audio_started = False + self._status = True # TODO: test for audio device + # These flags are referenced by audiograb + self.hw = self._parent.hw + self.running_sugar = self._parent.running_sugar + + def setup(self): + ''' set up audio-sensor-specific blocks ''' + self._sound = [0, 0] + self._volume = [0, 0] + self._pitch = [0, 0] + self._resistance = [0, 0] + self._voltage = [0, 0] + self.max_samples = 1500 + self.input_step = 1 + self.ringbuffer = [] + + palette = make_palette('extras', + colors=["#FF0000", "#A00000"], + help_string=_('Palette of extra options'), + position=8, + translation=_('extras')) + ''' + palette = make_palette('sensor', + colors=["#FF6060", "#A06060"], + help_string=_('Palette of sensor blocks'), + position=6) + ''' + hidden = True + ''' + if self._status: + hidden = False + ''' + + palette.add_block('sound', + hidden=hidden, + style='box-style', + label=_('sound'), + help_string=_('raw microphone input signal'), + value_block=True, + prim_name='sound') + palette.add_block('volume', + hidden=hidden, + style='box-style', + label=_('loudness'), + help_string=_('microphone input volume'), + value_block=True, + prim_name='volume') + + self._parent.lc.def_prim( + 'sound', 0, + Primitive(self.prim_sound, + return_type=TYPE_NUMBER, + kwarg_descs={'channel': ConstantArg(0)}, + call_afterwards=self.after_sound)) + + self._parent.lc.def_prim( + 'volume', 0, + Primitive(self.prim_volume, + return_type=TYPE_NUMBER, + kwarg_descs={'channel': ConstantArg(0)}, + call_afterwards=self.after_volume)) + + hidden = True + ''' + if PITCH_AVAILABLE and self._status: + hidden = False + ''' + + palette.add_block('pitch', + hidden=hidden, + style='box-style', + label=_('pitch'), + help_string=_('microphone input pitch'), + value_block=True, + prim_name='pitch') + self._parent.lc.def_prim( + 'pitch', 0, + Primitive(self.prim_pitch, + return_type=TYPE_NUMBER, + kwarg_descs={'channel': ConstantArg(0)}, + call_afterwards=self.after_pitch)) + + hidden = True + if self.hw in [XO1, XO15, XO175, XO4, XO30] and self._status: + # Calibration based on http://bugs.sugarlabs.org/ticket/4649 + if self.hw == XO1: + self.voltage_gain = 0.000022 + self.voltage_bias = 1.14 + elif self.hw == XO15: + self.voltage_gain = -0.00015 + self.voltage_bias = 1.70 + elif self.hw == XO175: # Range 0.01V to 3.01V + self.voltage_gain = 0.0000516 + self.voltage_bias = 1.3598 + elif self.hw == XO4: # Range 0.17V to 3.08V + self.voltage_gain = 0.0004073 + self.voltage_bias = 1.6289 + else: # XO 3.0 + self.voltage_gain = 0.000077 + self.voltage_bias = 0.72 + hidden = False + + palette.add_block('resistance', + hidden=hidden, + style='box-style', + label=_('resistance'), + help_string=_('microphone input resistance'), + prim_name='resistance') + palette.add_block('voltage', + hidden=hidden, + style='box-style', + label=_('voltage'), + help_string=_('microphone input voltage'), + prim_name='voltage') + + hidden = True + # Only add stereo capture for XO15 (broken on ARM #3675) + if self.hw in [XO15] and self._status: + hidden = False + + palette.add_block('resistance2', + hidden=hidden, + style='box-style', + label=_('resistance') + '2', + help_string=_('microphone input resistance'), + prim_name='resistance2') + palette.add_block('voltage2', + hidden=hidden, + style='box-style', + label=_('voltage') + '2', + help_string=_('microphone input voltage'), + prim_name='voltage2') + self._parent.lc.def_prim( + 'resistance', 0, + Primitive(self.prim_resistance, + return_type=TYPE_NUMBER, + kwarg_descs={'channel': ConstantArg(0)}, + call_afterwards=self.after_resistance)) + self._parent.lc.def_prim( + 'voltage', 0, + Primitive(self.prim_voltage, + return_type=TYPE_NUMBER, + kwarg_descs={'channel': ConstantArg(0)}, + call_afterwards=self.after_voltage)) + self._parent.lc.def_prim( + 'resistance2', 0, + Primitive(self.prim_resistance, + return_type=TYPE_NUMBER, + kwarg_descs={'channel': ConstantArg(1)}, + call_afterwards=self.after_resistance)) + self._parent.lc.def_prim( + 'voltage2', 0, + Primitive(self.prim_voltage, + return_type=TYPE_NUMBER, + kwarg_descs={'channel': ConstantArg(1)}, + call_afterwards=self.after_voltage)) + + if self.hw in [XO175, XO30, XO4]: + self.PARAMETERS = { + SENSOR_AC_BIAS: (False, True, 80, True), + SENSOR_DC_NO_BIAS: (True, False, 80, False), + SENSOR_DC_BIAS: (True, True, 90, False) + } + elif self.hw == XO15: + self.PARAMETERS = { + SENSOR_AC_BIAS: (False, True, 80, True), + SENSOR_DC_NO_BIAS: (True, False, 80, False), + SENSOR_DC_BIAS: (True, True, 90, False) + } + elif self.hw == XO1: + self.PARAMETERS = { + SENSOR_AC_BIAS: (False, True, 40, True), + SENSOR_DC_NO_BIAS: (True, False, 0, False), + SENSOR_DC_BIAS: (True, True, 0, False) + } + else: + self.PARAMETERS = { + SENSOR_AC_BIAS: (None, True, 40, True), + SENSOR_DC_NO_BIAS: (True, False, 80, False), + SENSOR_DC_BIAS: (True, True, 90, False) + } + + def start(self): + ''' Start grabbing audio if there is an audio block in use ''' + if not self._status: + return + self._sound = [0, 0] + self._volume = [0, 0] + self._pitch = [0, 0] + self._resistance = [0, 0] + self._voltage = [0, 0] + if self.audio_started: + self.audiograb.stop_grabbing() + if len(self._parent.block_list.get_similar_blocks( + 'block', ['volume', 'sound', 'pitch'])) > 0: + mode, bias, gain, boost = self.PARAMETERS[SENSOR_AC_BIAS] + elif len(self._parent.block_list.get_similar_blocks( + 'block', ['resistance', 'resistance2'])) > 0: + mode, bias, gain, boost = self.PARAMETERS[SENSOR_DC_BIAS] + elif len(self._parent.block_list.get_similar_blocks( + 'block', ['voltage', 'voltage2'])) > 0: + mode, bias, gain, boost = self.PARAMETERS[SENSOR_DC_NO_BIAS] + else: + return # No audio blocks in use. + self.audiograb = AudioGrab(self.new_buffer, self, + mode, bias, gain, boost) + self._channels = self.audiograb.channels + for i in range(self._channels): + self.ringbuffer.append(RingBuffer1d(self.max_samples, + dtype='int16')) + self.audiograb.start_grabbing() + self.audio_started = True + + def new_buffer(self, buf, channel=0): + ''' Append a new buffer to the ringbuffer ''' + self.ringbuffer[channel].append(buf) + return True + + def stop(self): + ''' This gets called by the stop button ''' + if self._status and self.audio_started: + self.audiograb.on_activity_quit() # reset all setting + self.audio_started = False + + def goto_background(self): + ''' This gets called when your process is sent to the background ''' + pass + + def return_to_foreground(self): + ''' This gets called when your process returns from the background ''' + pass + + def quit(self): + ''' This gets called by the quit button ''' + if self._status and self.audio_started: + self.audiograb.on_activity_quit() + + def _status_report(self): + debug_output( + 'Reporting audio sensor status: %s' % (str(self._status)), + self._parent.running_sugar) + return self._status + + # Block primitives + + def prim_sound(self, channel=0): + if not self._status: + return 0 + self._prim_sound(0) + # Return average of both channels if sampling in stereo + if self._channels == 2: + self._prim_sound(1) + return (self._sound[0] + self._sound[1]) / 2.0 + else: + return self._sound[0] + + def _prim_sound(self, channel): + ''' return raw mic in value ''' + buf = self.ringbuffer[channel].read(None, self.input_step) + if len(buf) > 0: + self._sound[channel] = float(buf[0]) + else: + self._sound[channel] = 0 + + def after_sound(self, channel=0): + if self._parent.lc.update_values: + self._parent.lc.update_label_value('sound', self._sound[channel]) + + def prim_volume(self, channel=0): + if not self._status: + return 0 + self._prim_volume(0) + # Return average of both channels if sampling in stereo + if self._channels == 2: + self._prim_volume(1) + return (self._volume[0] + self._volume[1]) / 2.0 + else: + return self._volume[0] + + def _prim_volume(self, channel): + ''' return raw mic in value ''' + buf = self.ringbuffer[channel].read(None, self.input_step) + if len(buf) > 0: + self._volume[channel] = float(_avg(buf, abs_value=True)) + else: + self._volume[channel] = 0 + + def after_volume(self, channel=0): + if self._parent.lc.update_values: + self._parent.lc.update_label_value('volume', self._volume[channel]) + + def prim_pitch(self, channel=0): + if not self._status: + return 0 + self._prim_pitch(0) + # Return average of both channels if sampling in stereo + if self._channels == 2: + self._prim_pitch(1) + return (self._pitch[0] + self._pitch[1]) / 2.0 + else: + return self._pitch[0] + + def _prim_pitch(self, channel): + ''' return raw mic in value ''' + buf = self.ringbuffer[channel].read(None, self.input_step) + if len(buf) > 0: + buf = rfft(buf) + buf = abs(buf) + maxi = buf.argmax() + if maxi == 0: + self._pitch[channel] = 0 + else: # Simple interpolation + a, b, c = buf[maxi - 1], buf[maxi], buf[maxi + 1] + maxi -= a / float(a + b + c) + maxi += c / float(a + b + c) + self._pitch[channel] = maxi * 48000 / (len(buf) * 2) + else: + self._pitch[channel] = 0 + + def after_pitch(self, channel=0): + if self._parent.lc.update_values: + self._parent.lc.update_label_value('pitch', self._pitch[channel]) + + def prim_resistance(self, channel=0): + if not self.hw in [XO1, XO15, XO175, XO30, XO4] or not self._status: + return 0 + if self.hw in [XO1, XO4]: + self._prim_resistance(0) + return self._resistance[0] + elif self.hw == XO15: + self._prim_resistance(channel) + return self._resistance[channel] + # For XO175: channel assignment is seemingly random + # (#3675), one of them will be 0 + else: + self._prim_resistance(0) + if self._resistance[0] != 999999999: + return self._resistance[0] + else: + self._prim_resistance(1) + return self._resistance[1] + + def _prim_resistance(self, channel): + ''' return resistance sensor value ''' + buf = self.ringbuffer[channel].read(None, self.input_step) + if len(buf) > 0: + # See http://bugs.sugarlabs.org/ticket/552#comment:7 + # and http://bugs.sugarlabs.org/ticket/4649 + avg_buf = float(_avg(buf)) + if self.hw == XO1: + self._resistance[channel] = \ + 2.718 ** ((avg_buf * 0.000045788) + 8.0531) + elif self.hw == XO15: + if avg_buf > 0: + self._resistance[channel] = (420000000 / avg_buf) - 13500 + else: + self._resistance[channel] = 420000000 + elif self.hw == XO175: # Range 0 to inf ohms + if avg_buf < 30519: + self._resistance[channel] = \ + (92000000. / (30519 - avg_buf)) - 1620 + else: + self._resistance[channel] = 999999999 + elif self.hw == XO4: # Range 0 to inf ohms + if avg_buf < 6629: + self._resistance[channel] = \ + (50000000. / (6629 - avg_buf)) - 3175 + else: + self._resistance[channel] = 999999999 + else: # XO 3.0 + if avg_buf < 30514: + self._resistance[channel] = \ + (46000000. / (30514 - avg_buf)) - 1150 + else: + self._resistance[channel] = 999999999 + if self._resistance[channel] < 0: + self._resistance[channel] = 0 + else: + self._resistance[channel] = 0 + + def after_resistance(self, channel=0): + if self._parent.lc.update_values: + self._parent.lc.update_label_value( + ['resistance', 'resistance2'][channel], + self._resistance[channel]) + + def prim_voltage(self, channel=0): + if not self.hw in [XO1, XO15, XO175, XO30, XO4] or not self._status: + return 0 + if self.hw in [XO1, XO4]: + self._prim_voltage(0) + return self._voltage[0] + elif self.hw == XO15: + self._prim_voltage(channel) + return self._voltage[channel] + # FIXME: For XO175: channel assignment is seemingly random + # (#3675), one of them will be 0 + else: + self._prim_voltage(0) + if self._voltage[0] != 0: + return self._voltage[0] + else: + self._prim_voltage(1) + return self._voltage[1] + + def _prim_voltage(self, channel): + ''' return voltage sensor value ''' + buf = self.ringbuffer[channel].read(None, self.input_step) + buf = self.ringbuffer[channel].read(None, self.input_step) + if len(buf) > 0: + # See + self._voltage[channel] = \ + float(_avg(buf)) * self.voltage_gain + self.voltage_bias + else: + self._voltage[channel] = 0 + + def after_voltage(self, channel=0): + if self._parent.lc.update_values: + self._parent.lc.update_label_value( + ['voltage', 'voltage2'][channel], + self._voltage[channel]) diff --git a/plugins/audio_sensors/audiograb.py b/plugins/audio_sensors/audiograb.py new file mode 100644 index 0000000..228e4c2 --- /dev/null +++ b/plugins/audio_sensors/audiograb.py @@ -0,0 +1,673 @@ +#! /usr/bin/python +# +# Author: Arjun Sarwal arjun@laptop.org +# Copyright (C) 2007, Arjun Sarwal +# Copyright (C) 2009-12 Walter Bender +# Copyright (C) 2009, Benjamin Berg, Sebastian Berg +# Copyright (C) 2009, Sayamindu Dasgupta +# Copyright (C) 2010, Sascha Silbe +# +# 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 2 of the License, or +# (at your option) any later version. +# +# You should have received a copy of the GNU General Public License +# along with this library; if not, write to the Free Software +# Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA + +import pygst +import gst +import gst.interfaces +from numpy import fromstring +import subprocess +import traceback +from string import find +from threading import Timer + +from TurtleArt.taconstants import XO1, XO4 +from TurtleArt.tautils import debug_output + +# Initial device settings +RATE = 48000 +MIC_BOOST = True +DC_MODE_ENABLE = False +CAPTURE_GAIN = 50 +BIAS = True + +# Setting on quit +QUIT_MIC_BOOST = False +QUIT_DC_MODE_ENABLE = False +QUIT_CAPTURE_GAIN = 100 +QUIT_BIAS = True + +# Capture modes +SENSOR_AC_NO_BIAS = 'external' +SENSOR_AC_BIAS = 'sound' +SENSOR_DC_NO_BIAS = 'voltage' +SENSOR_DC_BIAS = 'resistance' + + +class AudioGrab(): + """ The interface between measure and the audio device """ + + def __init__(self, callable1, parent, + mode=None, bias=None, gain=None, boost=None): + """ Initialize the class: callable1 is a data buffer; + parent is the parent class""" + + self.callable1 = callable1 + self.parent = parent + self.sensor = None + + self.temp_buffer = [0] + + self.rate = RATE + # Force XO1 and XO4 to use just 1 channel + if self.parent.hw in [XO1, XO4]: + self.channels = 1 + else: + self.channels = None + + self._dc_control = None + self._mic_bias_control = None + self._capture_control = None + self._mic_boost_control = None + self._labels_available = True # Query controls for device names + + self._query_mixer() + # If Channels was not found in the Capture controller, guess. + if self.channels is None: + debug_output('Guessing there are 2 channels', + self.parent.running_sugar) + self.channels = 2 + + # Set mixer to known state + self.set_dc_mode(DC_MODE_ENABLE) + self.set_bias(BIAS) + self.set_capture_gain(CAPTURE_GAIN) + self.set_mic_boost(MIC_BOOST) + + self.master = self.get_master() + self.dc_mode = self.get_dc_mode() + self.bias = self.get_bias() + self.capture_gain = self.get_capture_gain() + self.mic_boost = self.get_mic_boost() + + # Set mixer to desired state + self._set_sensor_type(mode, bias, gain, boost) + self.dc_mode = self.get_dc_mode() + self.bias = self.get_bias() + self.capture_gain = self.get_capture_gain() + self.mic_boost = self.get_mic_boost() + + # Set up gstreamer pipeline + self._pad_count = 0 + self.pads = [] + self.queue = [] + self.fakesink = [] + self.pipeline = gst.Pipeline('pipeline') + self.alsasrc = gst.element_factory_make('alsasrc', 'alsa-source') + self.pipeline.add(self.alsasrc) + self.caps1 = gst.element_factory_make('capsfilter', 'caps1') + self.pipeline.add(self.caps1) + caps_str = 'audio/x-raw-int,rate=%d,channels=%d,depth=16' % ( + RATE, self.channels) + self.caps1.set_property('caps', gst.caps_from_string(caps_str)) + if self.channels == 1: + self.fakesink.append(gst.element_factory_make('fakesink', 'fsink')) + self.pipeline.add(self.fakesink[0]) + self.fakesink[0].connect('handoff', self.on_buffer, 0) + self.fakesink[0].set_property('signal-handoffs', True) + gst.element_link_many(self.alsasrc, self.caps1, self.fakesink[0]) + else: + if not hasattr(self, 'splitter'): + self.splitter = gst.element_factory_make('deinterleave') + self.pipeline.add(self.splitter) + self.splitter.set_properties('keep-positions=true', 'name=d') + self.splitter.connect('pad-added', self._splitter_pad_added) + gst.element_link_many(self.alsasrc, self.caps1, self.splitter) + for i in range(self.channels): + self.queue.append(gst.element_factory_make('queue')) + self.pipeline.add(self.queue[i]) + self.fakesink.append(gst.element_factory_make('fakesink')) + self.pipeline.add(self.fakesink[i]) + self.fakesink[i].connect('handoff', self.on_buffer, i) + self.fakesink[i].set_property('signal-handoffs', True) + + self.dont_queue_the_buffer = False + + # Timer for interval sampling and switch to indicate when to capture + self.capture_timer = None + self.capture_interval_sample = False + + def _query_mixer(self): + self._mixer = gst.element_factory_make('alsamixer') + rc = self._mixer.set_state(gst.STATE_PAUSED) + assert rc == gst.STATE_CHANGE_SUCCESS + + # Query the available controls + tracks_list = self._mixer.list_tracks() + if hasattr(tracks_list[0].props, 'untranslated_label'): + self._capture_control = self._find_control(['capture', 'axi']) + self._dc_control = self._find_control(['dc mode']) + self._mic_bias_control = self._find_control(['mic bias', + 'dc input bias', + 'v_refout']) + self._mic_boost_control = self._find_control(['mic boost', + 'mic1 boost', + 'mic boost (+20db)', + 'internal mic boost', + 'analog mic boost']) + self._mic_gain_control = self._find_control(['mic']) + self._master_control = self._find_control(['master']) + else: # Use hardwired values + self._labels_available = False + + def _unlink_sink_queues(self): + ''' Build the sink pipelines ''' + + # If there were existing pipelines, unlink them + for i in range(self._pad_count): + try: + self.splitter.unlink(self.queue[i]) + self.queue[i].unlink(self.fakesink[i]) + except: + traceback.print_exc() + + # Build the new pipelines + self._pad_count = 0 + self.pads = [] + + def _splitter_pad_added(self, element, pad): + ''' Seems to be the case that ring is right channel 0, + tip is left channel 1''' + ''' + debug_output('splitter pad %d added' % (self._pad_count), + self.parent.running_sugar) + ''' + self.pads.append(pad) + if self._pad_count < self.channels: + pad.link(self.queue[self._pad_count].get_pad('sink')) + self.queue[self._pad_count].get_pad('src').link( + self.fakesink[self._pad_count].get_pad('sink')) + self._pad_count += 1 + else: + debug_output('ignoring channels > %d' % (self.channels), + self.parent.running_sugar) + + def set_handoff_signal(self, handoff_state): + '''Sets whether the handoff signal would generate an interrupt + or not''' + for i in range(len(self.fakesink)): + self.fakesink[i].set_property('signal-handoffs', handoff_state) + + def _new_buffer(self, buf, channel): + ''' Use a new buffer ''' + if not self.dont_queue_the_buffer: + self.temp_buffer = buf + self.callable1(buf, channel=channel) + else: + pass + + def on_buffer(self, element, data_buffer, pad, channel): + '''The function that is called whenever new data is available + This is the signal handler for the handoff signal''' + temp_buffer = fromstring(data_buffer, 'int16') + if not self.dont_queue_the_buffer: + self._new_buffer(temp_buffer, channel=channel) + return False + + def start_sound_device(self): + '''Start or Restart grabbing data from the audio capture''' + gst.event_new_flush_start() + self.pipeline.set_state(gst.STATE_PLAYING) + + def stop_sound_device(self): + '''Stop grabbing data from capture device''' + gst.event_new_flush_stop() + self.pipeline.set_state(gst.STATE_NULL) + + def sample_now(self): + ''' Log the current sample now. This method is called from the + capture_timer object when the interval expires. ''' + self.capture_interval_sample = True + self.make_timer() + + def set_buffer_interval_logging(self, interval=0): + '''Sets the number of buffers after which a buffer needs to be + emitted''' + self.buffer_interval_logging = interval + + def set_sampling_rate(self, sr): + '''Sets the sampling rate of the capture device + Sampling rate must be given as an integer for example 16000 for + setting 16Khz sampling rate + The sampling rate would be set in the device to the nearest available''' + self.pause_grabbing() + caps_str = 'audio/x-raw-int,rate=%d,channels=%d,depth=16' % ( + sr, self.channels) + self.caps1.set_property('caps', gst.caps_from_string(caps_str)) + self.resume_grabbing() + + def get_sampling_rate(self): + '''Gets the sampling rate of the capture device''' + return int(self.caps1.get_property('caps')[0]['rate']) + + def set_callable1(self, callable1): + '''Sets the callable to the drawing function for giving the + data at the end of idle-add''' + self.callable1 = callable1 + + def start_grabbing(self): + '''Called right at the start of the Activity''' + self.start_sound_device() + self.set_handoff_signal(True) + + def pause_grabbing(self): + '''When Activity goes into background''' + self.save_state() + self.stop_sound_device() + + def resume_grabbing(self): + '''When Activity becomes active after going to background''' + self.start_sound_device() + self.resume_state() + self.set_handoff_signal(True) + + def stop_grabbing(self): + '''Not used ???''' + self.stop_sound_device() + self.set_handoff_signal(False) + + def _find_control(self, prefixes): + '''Try to find a mixer control matching one of the prefixes. + + The control with the best match (smallest difference in length + between label and prefix) will be returned. If no match is found, + None is returned. + ''' + def best_prefix(label, prefixes): + matches =\ + [len(label) - len(p) for p in prefixes if label.startswith(p)] + if not matches: + return None + + matches.sort() + return matches[0] + + controls = [] + for track in self._mixer.list_tracks(): + label = track.props.untranslated_label.lower() + diff = best_prefix(label, prefixes) + if diff is not None: + controls.append((track, diff)) + + controls.sort(key=lambda e: e[1]) + if controls: + ''' + debug_output('Found control: %s' %\ + (str(controls[0][0].props.untranslated_label)), + self.parent.running_sugar) + ''' + if self.channels is None: + if hasattr(controls[0][0], 'num_channels'): + channels = controls[0][0].num_channels + if channels > 0: + self.channels = channels + ''' + debug_output('setting channels to %d' % (self.channels), + self.parent.running_sugar) + ''' + + return controls[0][0] + + return None + + def save_state(self): + '''Saves the state of all audio controls''' + self.master = self.get_master() + self.bias = self.get_bias() + self.dc_mode = self.get_dc_mode() + self.capture_gain = self.get_capture_gain() + self.mic_boost = self.get_mic_boost() + + def resume_state(self): + '''Put back all audio control settings from the saved state''' + self.set_master(self.master) + self.set_bias(self.bias) + self.set_dc_mode(self.dc_mode) + self.set_capture_gain(self.capture_gain) + self.set_mic_boost(self.mic_boost) + + def _get_mute(self, control, name, default): + '''Get mute status of a control''' + if not control: + return default + return bool(control.flags & gst.interfaces.MIXER_TRACK_MUTE) + + def _set_mute(self, control, name, value): + '''Mute a control''' + if not control: + return + self._mixer.set_mute(control, value) + + def _get_volume(self, control, name): + '''Get volume of a control and convert to a scale of 0-100''' + if not control: + return 100 + volume = self._mixer.get_volume(control) + if type(volume) == tuple: + hw_volume = volume[0] + else: + hw_volume = volume + min_vol = control.min_volume + max_vol = control.max_volume + if max_vol == min_vol: + percent = 100 + else: + percent = (hw_volume - min_vol) * 100 // (max_vol - min_vol) + return percent + + def _set_volume(self, control, name, value): + '''Sets the level of a control on a scale of 0-100''' + if not control: + return + # convert value to scale of control + min_vol = control.min_volume + max_vol = control.max_volume + if min_vol != max_vol: + hw_volume = value * (max_vol - min_vol) // 100 + min_vol + self._mixer.set_volume(control, + (hw_volume,) * control.num_channels) + + def amixer_set(self, control, state): + ''' Direct call to amixer for old systems. ''' + if state: + output = check_output( + ['amixer', 'set', "%s" % (control), 'unmute'], + 'Problem with amixer set "%s" unmute' % (control), + self.parent.running_sugar) + else: + output = check_output( + ['amixer', 'set', "%s" % (control), 'mute'], + 'Problem with amixer set "%s" mute' % (control), + self.parent.running_sugar) + + def mute_master(self): + '''Mutes the Master Control''' + if self._labels_available and self.parent.hw != XO1: + self._set_mute(self._master_control, 'Master', True) + else: + self.amixer_set('Master', False) + + def unmute_master(self): + '''Unmutes the Master Control''' + if self._labels_available and self.parent.hw != XO1: + self._set_mute(self._master_control, 'Master', True) + else: + self.amixer_set('Master', True) + + def set_master(self, master_val): + '''Sets the Master gain slider settings + master_val must be given as an integer between 0 and 100 indicating the + percentage of the slider to be set''' + if self._labels_available: + self._set_volume(self._master_control, 'Master', master_val) + else: + output = check_output( + ['amixer', 'set', 'Master', "%d%s" % (master_val, '%')], + 'Problem with amixer set Master', + self.parent.running_sugar) + + def get_master(self): + '''Gets the MIC gain slider settings. The value returned is an + integer between 0-100 and is an indicative of the percentage 0 - 100%''' + if self._labels_available: + return self._get_volume(self._master_control, 'master') + else: + output = check_output(['amixer', 'get', 'Master'], + 'amixer: Could not get Master volume', + self.parent.running_sugar) + if output is None: + return 100 + else: + output = output[find(output, 'Front Left:'):] + output = output[find(output, '[') + 1:] + output = output[:find(output, '%]')] + return int(output) + + def set_bias(self, bias_state=False): + '''Enables / disables bias voltage.''' + if self._labels_available and self.parent.hw != XO1: + if self._mic_bias_control is None: + return + # If there is a flag property, use set_mute + if self._mic_bias_control not in self._mixer.list_tracks() or \ + hasattr(self._mic_bias_control.props, 'flags'): + self._set_mute( + self._mic_bias_control, 'Mic Bias', not bias_state) + # We assume that values are sorted from lowest (=off) to highest. + # Since they are mixed strings ('Off', '50%', etc.), we cannot + # easily ensure this by sorting with the default sort order. + elif bias_state: # Otherwise, set with volume + self._mixer.set_volume(self._mic_bias_control, + self._mic_bias_control.max_volume) + else: + self._mixer.set_volume(self._mic_bias_control, + self._mic_bias_control.min_volume) + elif not self._labels_available: + self.amixer_set('V_REFOUT Enable', bias_state) + else: + self.amixer_set('MIC Bias Enable', bias_state) + + def get_bias(self): + '''Check whether bias voltage is enabled.''' + if self._labels_available: + if self._mic_bias_control is None: + return False + if self._mic_bias_control not in self._mixer.list_tracks() or \ + hasattr(self._mic_bias_control.props, 'flags'): + return not self._get_mute( + self._mic_bias_control, 'Mic Bias', False) + value = self._mixer.get_volume(self._mic_bias_control) + if value == self._mic_bias_control.min_volume: + return False + return True + else: + output = check_output(['amixer', 'get', "V_REFOUT Enable"], + 'amixer: Could not get mic bias voltage', + self.parent.running_sugar) + if output is None: + return False + else: + output = output[find(output, 'Mono:'):] + output = output[find(output, '[') + 1:] + output = output[:find(output, ']')] + if output == 'on': + return True + return False + + def set_dc_mode(self, dc_mode=False): + '''Sets the DC Mode Enable control + pass False to mute and True to unmute''' + if self._labels_available and self.parent.hw != XO1: + if self._dc_control is not None: + self._set_mute(self._dc_control, 'DC mode', not dc_mode) + else: + self.amixer_set('DC Mode Enable', dc_mode) + + def get_dc_mode(self): + '''Returns the setting of DC Mode Enable control + i.e. True: Unmuted and False: Muted''' + if self._labels_available and self.parent.hw != XO1: + if self._dc_control is not None: + return not self._get_mute(self._dc_control, 'DC mode', False) + else: + return False + else: + output = check_output(['amixer', 'get', "DC Mode Enable"], + 'amixer: Could not get DC Mode', + self.parent.running_sugar) + if output is None: + return False + else: + output = output[find(output, 'Mono:'):] + output = output[find(output, '[') + 1:] + output = output[:find(output, ']')] + if output == 'on': + return True + return False + + def set_mic_boost(self, mic_boost=False): + '''Set Mic Boost. + True = +20dB, False = 0dB''' + if self._labels_available: + if self._mic_boost_control is None: + return + # If there is a volume, use set volume + if hasattr(self._mic_boost_control, 'min_volume'): + if mic_boost: + self._set_volume(self._mic_boost_control, 'boost', 100) + else: + self._set_volume(self._mic_boost_control, 'boost', 0) + # Else if there is a flag property, use set_mute + elif self._mic_boost_control not in self._mixer.list_tracks() or \ + hasattr(self._mic_boost_control.props, 'flags'): + self._set_mute( + self._mic_boost_control, 'Mic Boost', not mic_boost) + else: + self.amixer_set('Mic Boost (+20dB)', mic_boost) + + def get_mic_boost(self): + '''Return Mic Boost setting. + True = +20dB, False = 0dB''' + if self._labels_available: + if self._mic_boost_control is None: + return False + if self._mic_boost_control not in self._mixer.list_tracks() or \ + hasattr(self._mic_boost_control.props, 'flags'): + return not self._get_mute( + self._mic_boost_control, 'Mic Boost', False) + else: # Compare to min value + value = self._mixer.get_volume(self._mic_boost_control) + if value != self._mic_boost_control.min_volume: + return True + return False + else: + output = check_output(['amixer', 'get', "Mic Boost (+20dB)"], + 'amixer: Could not get mic boost', + self.parent.running_sugar) + if output is None: + return False + else: + output = output[find(output, 'Mono:'):] + output = output[find(output, '[') + 1:] + output = output[:find(output, ']')] + if output == 'on': + return True + return False + + def set_capture_gain(self, capture_val): + '''Sets the Capture gain slider settings + capture_val must be given as an integer between 0 and 100 indicating the + percentage of the slider to be set''' + if self._labels_available and self.parent.hw != XO1: + if self._capture_control is not None: + self._set_volume(self._capture_control, 'Capture', capture_val) + else: + output = check_output( + ['amixer', 'set', 'Capture', "%d%s" % (capture_val, '%')], + 'Problem with amixer set Capture', + self.parent.running_sugar) + + def get_capture_gain(self): + '''Gets the Capture gain slider settings. The value returned is an + integer between 0-100 and is an indicative of the percentage 0 - 100%''' + if self._labels_available: + if self._capture_control is not None: + return self._get_volume(self._capture_control, 'Capture') + else: + return 0 + else: + output = check_output(['amixer', 'get', 'Capture'], + 'amixer: Could not get Capture level', + self.parent.running_sugar) + if output is None: + return 100 + else: + output = output[find(output, 'Front Left:'):] + output = output[find(output, '[') + 1:] + output = output[:find(output, '%]')] + return int(output) + + def set_mic_gain(self, mic_val): + '''Sets the MIC gain slider settings + mic_val must be given as an integer between 0 and 100 indicating the + percentage of the slider to be set''' + if self._labels_available and self.parent.hw != XO1: + self._set_volume(self._mic_gain_control, 'Mic', mic_val) + else: + output = check_output( + ['amixer', 'set', 'Mic', "%d%s" % (mic_val, '%')], + 'Problem with amixer set Mic', + self.parent.running_sugar) + + def get_mic_gain(self): + '''Gets the MIC gain slider settings. The value returned is an + integer between 0-100 and is an indicative of the percentage 0 - 100%''' + if self._labels_available and self.parent.hw != XO1: + return self._get_volume(self._mic_gain_control, 'Mic') + else: + output = check_output(['amixer', 'get', 'Mic'], + 'amixer: Could not get mic gain level', + self.parent.running_sugar) + if output is None: + return 100 + else: + output = output[find(output, 'Mono:'):] + output = output[find(output, '[') + 1:] + output = output[:find(output, '%]')] + return int(output) + + def _set_sensor_type(self, mode=None, bias=None, gain=None, boost=None): + '''Helper to modify (some) of the sensor settings.''' + if mode is not None: + self.set_dc_mode(mode) + if bias is not None: + self.set_bias(bias) + if gain is not None: + self.set_capture_gain(gain) + if boost is not None: + self.set_mic_boost(boost) + self.save_state() + + def on_activity_quit(self): + '''When Activity quits''' + self.set_mic_boost(QUIT_MIC_BOOST) + self.set_dc_mode(QUIT_DC_MODE_ENABLE) + self.set_capture_gain(QUIT_CAPTURE_GAIN) + self.set_bias(QUIT_BIAS) + self.stop_sound_device() + + +def check_output(command, warning, running_sugar=True): + ''' Workaround for old systems without subprocess.check_output''' + if hasattr(subprocess, 'check_output'): + try: + output = subprocess.check_output(command) + except subprocess.CalledProcessError: + debug_output(warning, running_sugar) + return None + else: + import commands + + cmd = '' + for c in command: + cmd += c + cmd += ' ' + (status, output) = commands.getstatusoutput(cmd) + if status != 0: + debug_output(warning, running_sugar) + return None + return output diff --git a/plugins/audio_sensors/icons/extrasoff.svg b/plugins/audio_sensors/icons/extrasoff.svg new file mode 100644 index 0000000..a956f03 --- /dev/null +++ b/plugins/audio_sensors/icons/extrasoff.svg @@ -0,0 +1,75 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/plugins/audio_sensors/icons/extrason.svg b/plugins/audio_sensors/icons/extrason.svg new file mode 100644 index 0000000..7ee08bf --- /dev/null +++ b/plugins/audio_sensors/icons/extrason.svg @@ -0,0 +1,158 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/audio_sensors/icons/sensoroff.svg b/plugins/audio_sensors/icons/sensoroff.svg new file mode 100644 index 0000000..0a16670 --- /dev/null +++ b/plugins/audio_sensors/icons/sensoroff.svg @@ -0,0 +1,79 @@ + + + +image/svg+xml + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/audio_sensors/icons/sensoron.svg b/plugins/audio_sensors/icons/sensoron.svg new file mode 100644 index 0000000..d756860 --- /dev/null +++ b/plugins/audio_sensors/icons/sensoron.svg @@ -0,0 +1,63 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/plugins/audio_sensors/ringbuffer.py b/plugins/audio_sensors/ringbuffer.py new file mode 100644 index 0000000..2afb5c9 --- /dev/null +++ b/plugins/audio_sensors/ringbuffer.py @@ -0,0 +1,108 @@ +# Copyright (C) 2009, Benjamin Berg, Sebastian Berg +# Copyright (C) 2010, Walter Bender +# +# 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 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +import numpy as np + + +class RingBuffer1d(object): + """This class implements an array being written in as a ring and that can + be read from continuously ending with the newest data or starting with the + oldest. It returns a numpy array copy of the data; + """ + + def __init__(self, length, dtype=None): + """Initialize the 1 dimensional ring buffer with the given lengths. + The initial values are all 0s + """ + self.offset = 0 + + self._data = np.zeros(length, dtype=dtype) + + self.stored = 0 + + def fill(self, number): + self._data.fill(number) + self.offset = 0 + + def append(self, data): + """Append to the ring buffer (and overwrite old data). If len(data) + is greater then the ring buffers length, the newest data takes + precedence. + """ + data = np.asarray(data) + + if len(self._data) == 0: + return + + if len(data) >= len(self._data): + self._data[:] = data[-len(self._data):] + self.offset = 0 + self.stored = len(self._data) + + elif len(self._data) - self.offset >= len(data): + self._data[self.offset: self.offset + len(data)] = data + self.offset = self.offset + len(data) + self.stored += len(data) + else: + self._data[self.offset:] = data[:len(self._data) - self.offset] + self._data[:len(data) - (len(self._data) - self.offset)] = \ + data[-len(data) + (len(self._data) - self.offset):] + self.offset = len(data) - (len(self._data) - self.offset) + self.stored += len(data) + + if len(self._data) <= self.stored: + self.read = self._read + + def read(self, number=None, step=1): + """Read the ring Buffer. Number can be positive or negative. + Positive values will give the latest information, negative values will + give the newest added information from the buffer. (in normal order) + + Before the buffer is filled once: This returns just None + """ + return np.array([]) + + def _read(self, number=None, step=1): + """Read the ring Buffer. Number can be positive or negative. + Positive values will give the latest information, negative values will + give the newest added information from the buffer. (in normal order) + """ + if number == None: + number = len(self._data) // step + + number *= step + assert abs(number) <= len(self._data), \ + 'Number to read*step must be smaller then length' + + if number < 0: + if abs(number) <= self.offset: + return self._data[self.offset + number:self.offset:step] + + spam = (self.offset - 1) % step + + return np.concatenate( + (self._data[step - spam - 1 + self.offset + number::step], + self._data[spam:self.offset:step])) + + if number - (len(self._data) - self.offset) > 0: + spam = ((self.offset + number) - self.offset - 1) % step + return np.concatenate( + (self._data[self.offset:self.offset + number:step], + self._data[spam:number - + (len(self._data) - self.offset):step])) + + return self._data[self.offset:self.offset + number:step].copy() diff --git a/plugins/camera_sensor/camera_sensor.py b/plugins/camera_sensor/camera_sensor.py new file mode 100644 index 0000000..cb1e226 --- /dev/null +++ b/plugins/camera_sensor/camera_sensor.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python +#Copyright (c) 2011, 2012 Walter Bender +# +# 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 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gst +import gtk +from fcntl import ioctl +import os +from time import time + +from gettext import gettext as _ + +from plugins.camera_sensor.tacamera import Camera +from plugins.camera_sensor.v4l2 import v4l2_control, V4L2_CID_AUTOGAIN, \ + VIDIOC_G_CTRL, VIDIOC_S_CTRL + +from plugins.plugin import Plugin + +from TurtleArt.tapalette import make_palette +from TurtleArt.talogo import media_blocks_dictionary +from TurtleArt.tautils import get_path, debug_output +from TurtleArt.taconstants import MEDIA_SHAPES, NO_IMPORT, SKIN_PATHS, \ + BLOCKS_WITH_SKIN +from TurtleArt.taprimitive import (ConstantArg, Primitive) +from TurtleArt.tatype import TYPE_NUMBER + + +class Camera_sensor(Plugin): + + def __init__(self, parent): + Plugin.__init__(self) + ''' Make sure there is a camera device ''' + self._parent = parent + self._status = False + self._ag_control = None + self.devices = [] + self.cameras = [] + self.luminance = 0 + + if os.path.exists('/dev/video0'): + self.devices.append('/dev/video0') + if os.path.exists('/dev/video1'): + self.devices.append('/dev/video1') + if len(self.devices) > 0: + self._status = True + else: + self._status = False + + def setup(self): + ''' Set up the palettes ''' + ''' + sensors_palette = make_palette('sensor', + colors=["#FF6060", "#A06060"], + help_string=_( + 'Palette of sensor blocks'), + position=6) + ''' + media_palette = make_palette('media', + colors=["#A0FF00", "#80A000"], + help_string=_('Palette of media objects'), + position=7) + sensors_palette = media_palette + + # set up camera-specific blocks + media_blocks_dictionary['camera'] = self.prim_take_picture0 + media_blocks_dictionary['camera1'] = self.prim_take_picture1 + + SKIN_PATHS.append('plugins/camera_sensor/images') + + if self._status: + sensors_palette.add_block('luminance', + hidden=True, + style='box-style', + label=_('brightness'), + help_string=_( + 'light level detected by camera'), + value_block=True, + prim_name='luminance') + self._parent.lc.def_prim( + 'luminance', 0, + Primitive(self.prim_read_camera, + return_type=TYPE_NUMBER, + kwarg_descs={'luminance_only': ConstantArg(True)}, + call_afterwards=self.after_luminance)) + + # Depreciated block + sensors_palette.add_block('read_camera', + hidden=True, + style='box-style', + label=_('brightness'), + help_string=_( + 'Average RGB color from camera \ +is pushed to the stack'), + value_block=True, + prim_name='read_camera') + self._parent.lc.def_prim( + 'read_camera', 0, + Primitive(self.prim_read_camera, + kwarg_descs={'luminance_only': ConstantArg(False)})) + + media_palette.add_block('camera', + hidden=True, + style='box-style-media', + label=' ', + default='CAMERA', + help_string=_('camera output'), + content_block=True) + if len(self.devices) > 1: + media_palette.add_block('camera1', + hidden=True, + style='box-style-media', + label=' ', + default='CAMERA', + help_string=_('camera output'), + content_block=True) + else: + media_palette.add_block('camera1', + hidden=True, + style='box-style-media', + label=' ', + default='CAMERA', + help_string=_('camera output'), + content_block=True) + + else: # No camera, so blocks should do nothing + sensors_palette.add_block('luminance', + hidden=True, + style='box-style', + label=_('brightness'), + help_string=\ + _('light level detected by camera'), + value_block=True, + prim_name='read_camera') + self._parent.lc.def_prim( + 'luminance', 0, + Primitive(self.prim_read_camera, + return_type=TYPE_NUMBER, + kwarg_descs={'luminance_only': ConstantArg(True)}, + call_afterwards=self.after_luminance)) + + # Depreciated block + sensors_palette.add_block('read_camera', + hidden=True, + style='box-style', + label=_('brightness'), + help_string=_( + 'Average RGB color from camera \ +is pushed to the stack'), + value_block=True, + prim_name='read_camera') + self._parent.lc.def_prim( + 'read_camera', 0, + Primitive(self.prim_read_camera, + return_type=TYPE_NUMBER, + kwarg_descs={'luminance_only': ConstantArg(False)})) + + media_palette.add_block('camera', + hidden=True, + style='box-style-media', + label=' ', + default='CAMERA', + help_string=_('camera output'), + content_block=True) + + media_palette.add_block('camera1', + hidden=True, + style='box-style-media', + label=' ', + default='CAMERA', + help_string=_('camera output'), + content_block=True) + + NO_IMPORT.append('camera') + BLOCKS_WITH_SKIN.append('camera') + NO_IMPORT.append('camera1') + BLOCKS_WITH_SKIN.append('camera1') + MEDIA_SHAPES.append('camerasmall') + MEDIA_SHAPES.append('cameraoff') + MEDIA_SHAPES.append('camera1small') + MEDIA_SHAPES.append('camera1off') + + def start(self): + ''' Initialize the camera if there is an camera block in use ''' + if len(self._parent.block_list.get_similar_blocks('block', + ['camera', 'camera1', 'read_camera', 'luminance'])) > 0: + if self._status and len(self.cameras) == 0: + for device in self.devices: + self.cameras.append(Camera(device)) + + def quit(self): + ''' This gets called when the activity quits ''' + self._reset_the_camera() + + def stop(self): + ''' This gets called by the stop button ''' + self._reset_the_camera() + + def clear(self): + ''' This gets called by the clean button and erase button ''' + self._reset_the_camera() + + def _reset_the_camera(self): + if self._status and len(self.cameras) > 0: + for i, camera in enumerate(self.cameras): + camera.stop_camera_input() + self._set_autogain(1, camera=i) # enable AUTOGAIN + + def _status_report(self): + debug_output('Reporting camera status: %s' % (str(self._status)), + self._parent.running_sugar) + return self._status + + # Block primitives used in talogo + + def prim_take_picture0(self): + self._take_picture(camera=0) + + def prim_take_picture1(self): + self._take_picture(camera=1) + + def _take_picture(self, camera=0): + ''' method called by media block ''' + self._set_autogain(1, camera) # enable AUTOGAIN + self._get_pixbuf_from_camera(camera) + self._parent.lc.pixbuf = self.cameras[camera].pixbuf + + def prim_read_camera(self, luminance_only=False, camera=0): + """ Read average pixel from camera and push b, g, r to the stack """ + self.luminance_only = luminance_only + if not self._status: + if self.luminance_only: + return -1 + else: + self._parent.lc.heap.append(-1) + self._parent.lc.heap.append(-1) + self._parent.lc.heap.append(-1) + return + + array = None + self._set_autogain(0, camera=camera) # disable AUTOGAIN + self._get_pixbuf_from_camera(camera=camera) + self.calc_luminance(camera=camera) + if self.luminance_only: + return int(self.luminance) + else: + self._parent.lc.heap.append(self.b) + self._parent.lc.heap.append(self.g) + self._parent.lc.heap.append(self.r) + return + + def calc_luminance(self, camera=0): + array = self.cameras[camera].pixbuf.get_pixels() + width = self.cameras[camera].pixbuf.get_width() + height = self.cameras[camera].pixbuf.get_height() + + if array is not None: + length = int(len(array) / 3) + if length != width * height: + debug_output('array length != width x height (%d != %dx%d)' % \ + (length, width, height), + self._parent.running_sugar) + + # Average the 100 pixels in the center of the screen + r, g, b = 0, 0, 0 + row_offset = int((height / 2 - 5) * width * 3) + column_offset = int(width / 2 - 5) * 3 + for y in range(10): + i = row_offset + column_offset + for x in range(10): + r += ord(array[i]) + i += 1 + g += ord(array[i]) + i += 1 + b += ord(array[i]) + i += 1 + row_offset += width * 3 + if self.luminance_only: + self.luminance = int((r * 0.3 + g * 0.6 + b * 0.1) / 100) + else: + self.r = int(r / 100) + self.g = int(g / 100) + self.b = int(b / 100) + else: + if self.luminance_only: + self.luminance = -1 + else: + self.r = -1 + self.g = -1 + self.b = -1 + + def after_luminance(self, luminance_only=False): + if self._parent.lc.update_values and luminance_only: + self._parent.lc.update_label_value('luminance', self.luminance) + + def _set_autogain(self, state, camera=0): + ''' 0 is off; 1 is on ''' + if self._ag_control is not None and self._ag_control.value == state: + return + try: + video_capture_device = open(self.devices[camera], 'rw') + except: + video_capture_device = None + debug_output('video capture device not available', + self._parent.running_sugar) + return + self._ag_control = v4l2_control(V4L2_CID_AUTOGAIN) + try: + ioctl(video_capture_device, VIDIOC_G_CTRL, self._ag_control) + self._ag_control.value = state + ioctl(video_capture_device, VIDIOC_S_CTRL, self._ag_control) + except: + pass + video_capture_device.close() + + def _get_pixbuf_from_camera(self, camera): + ''' Regardless of how we get it, we want to return a pixbuf ''' + self._parent.lc.pixbuf = None + if self._status: + self.cameras[camera].start_camera_input() diff --git a/plugins/camera_sensor/glive.py b/plugins/camera_sensor/glive.py new file mode 100644 index 0000000..9f8c5fa --- /dev/null +++ b/plugins/camera_sensor/glive.py @@ -0,0 +1,638 @@ +#Copyright (c) 2008, Media Modifications Ltd. + +#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 os +from gettext import gettext as _ +import time + +import gtk +import gst +import pygst +pygst.require('0.10') +import gobject +gobject.threads_init() + +from sugar.activity.activity import get_bundle_path +import logging + +from instance import Instance +import constants +import utils + +logger = logging.getLogger('glive') + +OGG_TRAITS = { + 0: { 'width': 160, 'height': 120, 'quality': 16 }, + 1: { 'width': 384, 'height': 288, 'quality': 16 } } + +class Glive: + PHOTO_MODE_PHOTO = 0 + PHOTO_MODE_AUDIO = 1 + + def __init__(self, activity_obj, model): + self.activity = activity_obj + self.model = model + self._eos_cb = None + + self._has_camera = False + self._can_limit_framerate = False + self._playing = False + self._pic_exposure_open = False + self._thumb_exposure_open = False + self._photo_mode = self.PHOTO_MODE_PHOTO + + self._audio_transcode_handler = None + self._transcode_id = None + self._video_transcode_handler = None + self._thumb_handoff_handler = None + + self._audio_pixbuf = None + + self._detect_camera() + + self._pipeline = gst.Pipeline("Record") + self._create_photobin() + self._create_audiobin() + self._create_videobin() + self._create_xbin() + self._create_pipeline() + + self._thumb_pipes = [] + self._mux_pipes = [] + + bus = self._pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message', self._bus_message_handler) + + def _detect_camera(self): + v4l2src = gst.element_factory_make('v4l2src') + if v4l2src.props.device_name is None: + return + + self._has_camera = True + + # Figure out if we can place a framerate limit on the v4l2 element, + # which in theory will make it all the way down to the hardware. + # ideally, we should be able to do this by checking caps. However, I + # can't find a way to do this (at this time, XO-1 cafe camera driver + # doesn't support framerate changes, but gstreamer caps suggest + # otherwise) + pipeline = gst.Pipeline() + caps = gst.Caps('video/x-raw-yuv,framerate=10/1') + fsink = gst.element_factory_make('fakesink') + pipeline.add(v4l2src, fsink) + v4l2src.link(fsink, caps) + self._can_limit_framerate = pipeline.set_state(gst.STATE_PAUSED) != gst.STATE_CHANGE_FAILURE + pipeline.set_state(gst.STATE_NULL) + + def get_has_camera(self): + return self._has_camera + + def _create_photobin(self): + queue = gst.element_factory_make("queue", "pbqueue") + queue.set_property("leaky", True) + queue.set_property("max-size-buffers", 1) + + colorspace = gst.element_factory_make("ffmpegcolorspace", "pbcolorspace") + jpeg = gst.element_factory_make("jpegenc", "pbjpeg") + + sink = gst.element_factory_make("fakesink", "pbsink") + sink.connect("handoff", self._photo_handoff) + sink.set_property("signal-handoffs", True) + + self._photobin = gst.Bin("photobin") + self._photobin.add(queue, colorspace, jpeg, sink) + + gst.element_link_many(queue, colorspace, jpeg, sink) + + pad = queue.get_static_pad("sink") + self._photobin.add_pad(gst.GhostPad("sink", pad)) + + def _create_audiobin(self): + src = gst.element_factory_make("alsasrc", "absrc") + + # attempt to use direct access to the 0,0 device, solving some A/V + # sync issues + src.set_property("device", "plughw:0,0") + hwdev_available = src.set_state(gst.STATE_PAUSED) != gst.STATE_CHANGE_FAILURE + src.set_state(gst.STATE_NULL) + if not hwdev_available: + src.set_property("device", "default") + + srccaps = gst.Caps("audio/x-raw-int,rate=16000,channels=1,depth=16") + + # guarantee perfect stream, important for A/V sync + rate = gst.element_factory_make("audiorate") + + # without a buffer here, gstreamer struggles at the start of the + # recording and then the A/V sync is bad for the whole video + # (possibly a gstreamer/ALSA bug -- even if it gets caught up, it + # should be able to resync without problem) + queue = gst.element_factory_make("queue", "audioqueue") + queue.set_property("leaky", True) # prefer fresh data + queue.set_property("max-size-time", 5000000000) # 5 seconds + queue.set_property("max-size-buffers", 500) + queue.connect("overrun", self._log_queue_overrun) + + enc = gst.element_factory_make("wavenc", "abenc") + + sink = gst.element_factory_make("filesink", "absink") + sink.set_property("location", os.path.join(Instance.instancePath, "output.wav")) + + self._audiobin = gst.Bin("audiobin") + self._audiobin.add(src, rate, queue, enc, sink) + + src.link(rate, srccaps) + gst.element_link_many(rate, queue, enc, sink) + + def _create_videobin(self): + queue = gst.element_factory_make("queue", "videoqueue") + queue.set_property("max-size-time", 5000000000) # 5 seconds + queue.set_property("max-size-bytes", 33554432) # 32mb + queue.connect("overrun", self._log_queue_overrun) + + scale = gst.element_factory_make("videoscale", "vbscale") + + scalecapsfilter = gst.element_factory_make("capsfilter", "scalecaps") + + scalecaps = gst.Caps('video/x-raw-yuv,width=160,height=120') + scalecapsfilter.set_property("caps", scalecaps) + + colorspace = gst.element_factory_make("ffmpegcolorspace", "vbcolorspace") + + enc = gst.element_factory_make("theoraenc", "vbenc") + enc.set_property("quality", 16) + + mux = gst.element_factory_make("oggmux", "vbmux") + + sink = gst.element_factory_make("filesink", "vbfile") + sink.set_property("location", os.path.join(Instance.instancePath, "output.ogg")) + + self._videobin = gst.Bin("videobin") + self._videobin.add(queue, scale, scalecapsfilter, colorspace, enc, mux, sink) + + queue.link(scale) + scale.link_pads(None, scalecapsfilter, "sink") + scalecapsfilter.link_pads("src", colorspace, None) + gst.element_link_many(colorspace, enc, mux, sink) + + pad = queue.get_static_pad("sink") + self._videobin.add_pad(gst.GhostPad("sink", pad)) + + def _create_xbin(self): + scale = gst.element_factory_make("videoscale") + cspace = gst.element_factory_make("ffmpegcolorspace") + xsink = gst.element_factory_make("ximagesink", "xsink") + xsink.set_property("force-aspect-ratio", True) + + # http://thread.gmane.org/gmane.comp.video.gstreamer.devel/29644 + xsink.set_property("sync", False) + + self._xbin = gst.Bin("xbin") + self._xbin.add(scale, cspace, xsink) + gst.element_link_many(scale, cspace, xsink) + + pad = scale.get_static_pad("sink") + self._xbin.add_pad(gst.GhostPad("sink", pad)) + + def _config_videobin(self, quality, width, height): + vbenc = self._videobin.get_by_name("vbenc") + vbenc.set_property("quality", 16) + scaps = self._videobin.get_by_name("scalecaps") + scaps.set_property("caps", gst.Caps("video/x-raw-yuv,width=%d,height=%d" % (width, height))) + + def _create_pipeline(self): + if not self._has_camera: + return + + src = gst.element_factory_make("v4l2src", "camsrc") + try: + # old gst-plugins-good does not have this property + src.set_property("queue-size", 2) + except: + pass + + # if possible, it is important to place the framerate limit directly + # on the v4l2src so that it gets communicated all the way down to the + # camera level + if self._can_limit_framerate: + srccaps = gst.Caps('video/x-raw-yuv,framerate=10/1') + else: + srccaps = gst.Caps('video/x-raw-yuv') + + # we attempt to limit the framerate on the v4l2src directly, but we + # can't trust this: perhaps we are falling behind in our capture, + # or maybe the kernel driver doesn't provide the exact framerate. + # the videorate element guarantees a perfect framerate and is important + # for A/V sync because OGG does not store timestamps, it just stores + # the FPS value. + rate = gst.element_factory_make("videorate") + ratecaps = gst.Caps('video/x-raw-yuv,framerate=10/1') + + tee = gst.element_factory_make("tee", "tee") + queue = gst.element_factory_make("queue", "dispqueue") + + # prefer fresh frames + queue.set_property("leaky", True) + queue.set_property("max-size-buffers", 2) + + self._pipeline.add(src, rate, tee, queue) + src.link(rate, srccaps) + rate.link(tee, ratecaps) + tee.link(queue) + + self._xvsink = gst.element_factory_make("xvimagesink", "xsink") + self._xv_available = self._xvsink.set_state(gst.STATE_PAUSED) != gst.STATE_CHANGE_FAILURE + self._xvsink.set_state(gst.STATE_NULL) + + # http://thread.gmane.org/gmane.comp.video.gstreamer.devel/29644 + self._xvsink.set_property("sync", False) + + self._xvsink.set_property("force-aspect-ratio", True) + + def _log_queue_overrun(self, queue): + cbuffers = queue.get_property("current-level-buffers") + cbytes = queue.get_property("current-level-bytes") + ctime = queue.get_property("current-level-time") + logger.error("Buffer overrun in %s (%d buffers, %d bytes, %d time)" + % (queue.get_name(), cbuffers, cbytes, ctime)) + + def _thumb_element(self, name): + return self._thumb_pipes[-1].get_by_name(name) + + def is_using_xv(self): + return self._pipeline.get_by_name("xsink") == self._xvsink + + def _configure_xv(self): + if self.is_using_xv(): + # nothing to do, Xv already configured + return self._xvsink + + queue = self._pipeline.get_by_name("dispqueue") + if self._pipeline.get_by_name("xbin"): + # X sink is configured, so remove it + queue.unlink(self._xbin) + self._pipeline.remove(self._xbin) + + self._pipeline.add(self._xvsink) + queue.link(self._xvsink) + return self._xvsink + + def _configure_x(self): + if self._pipeline.get_by_name("xbin") == self._xbin: + # nothing to do, X already configured + return self._xbin.get_by_name("xsink") + + queue = self._pipeline.get_by_name("dispqueue") + xvsink = self._pipeline.get_by_name("xsink") + + if xvsink: + # Xv sink is configured, so remove it + queue.unlink(xvsink) + self._pipeline.remove(xvsink) + + self._pipeline.add(self._xbin) + queue.link(self._xbin) + return self._xbin.get_by_name("xsink") + + def play(self, use_xv=True): + if self._get_state() == gst.STATE_PLAYING: + return + + if self._has_camera: + if use_xv and self._xv_available: + xsink = self._configure_xv() + else: + xsink = self._configure_x() + + # X overlay must be set every time, it seems to forget when you stop + # the pipeline. + self.activity.set_glive_sink(xsink) + + self._pipeline.set_state(gst.STATE_PLAYING) + self._playing = True + + def pause(self): + self._pipeline.set_state(gst.STATE_PAUSED) + self._playing = False + + def stop(self): + self._pipeline.set_state(gst.STATE_NULL) + self._playing = False + + def is_playing(self): + return self._playing + + def _get_state(self): + return self._pipeline.get_state()[1] + + def stop_recording_audio(self): + # We should be able to simply pause and remove the audiobin, but + # this seems to cause a gstreamer segfault. So we stop the whole + # pipeline while manipulating it. + # http://dev.laptop.org/ticket/10183 + self._pipeline.set_state(gst.STATE_NULL) + self.model.shutter_sound() + self._pipeline.remove(self._audiobin) + + audio_path = os.path.join(Instance.instancePath, "output.wav") + if not os.path.exists(audio_path) or os.path.getsize(audio_path) <= 0: + # FIXME: inform model of failure? + return + + if self._audio_pixbuf: + self.model.still_ready(self._audio_pixbuf) + + line = 'filesrc location=' + audio_path + ' name=audioFilesrc ! wavparse name=audioWavparse ! audioconvert name=audioAudioconvert ! vorbisenc name=audioVorbisenc ! oggmux name=audioOggmux ! filesink name=audioFilesink' + audioline = gst.parse_launch(line) + + taglist = self._get_tags(constants.TYPE_AUDIO) + + if self._audio_pixbuf: + pixbuf_b64 = utils.getStringEncodedFromPixbuf(self._audio_pixbuf) + taglist[gst.TAG_EXTENDED_COMMENT] = "coverart=" + pixbuf_b64 + + vorbis_enc = audioline.get_by_name('audioVorbisenc') + vorbis_enc.merge_tags(taglist, gst.TAG_MERGE_REPLACE_ALL) + + audioFilesink = audioline.get_by_name('audioFilesink') + audioOggFilepath = os.path.join(Instance.instancePath, "output.ogg") + audioFilesink.set_property("location", audioOggFilepath) + + audioBus = audioline.get_bus() + audioBus.add_signal_watch() + self._audio_transcode_handler = audioBus.connect('message', self._onMuxedAudioMessageCb, audioline) + self._transcode_id = gobject.timeout_add(200, self._transcodeUpdateCb, audioline) + audioline.set_state(gst.STATE_PLAYING) + + def _get_tags(self, type): + tl = gst.TagList() + tl[gst.TAG_ARTIST] = self.model.get_nickname() + tl[gst.TAG_COMMENT] = "olpc" + #this is unfortunately, unreliable + #record.Record.log.debug("self.ca.metadata['title']->" + str(self.ca.metadata['title']) ) + tl[gst.TAG_ALBUM] = "olpc" #self.ca.metadata['title'] + tl[gst.TAG_DATE] = utils.getDateString(int(time.time())) + stringType = constants.MEDIA_INFO[type]['istr'] + + # Translators: photo by photographer, e.g. "Photo by Mary" + tl[gst.TAG_TITLE] = _('%(type)s by %(name)s') % {'type': stringType, + 'name': self.model.get_nickname()} + return tl + + def _take_photo(self, photo_mode): + if self._pic_exposure_open: + return + + self._photo_mode = photo_mode + self._pic_exposure_open = True + pad = self._photobin.get_static_pad("sink") + self._pipeline.add(self._photobin) + self._photobin.set_state(gst.STATE_PLAYING) + self._pipeline.get_by_name("tee").link(self._photobin) + + def take_photo(self): + if self._has_camera: + self._take_photo(self.PHOTO_MODE_PHOTO) + + def _photo_handoff(self, fsink, buffer, pad, user_data=None): + if not self._pic_exposure_open: + return + + pad = self._photobin.get_static_pad("sink") + self._pipeline.get_by_name("tee").unlink(self._photobin) + self._pipeline.remove(self._photobin) + + self._pic_exposure_open = False + pic = gtk.gdk.pixbuf_loader_new_with_mime_type("image/jpeg") + pic.write( buffer ) + pic.close() + pixBuf = pic.get_pixbuf() + del pic + + self.save_photo(pixBuf) + + def save_photo(self, pixbuf): + if self._photo_mode == self.PHOTO_MODE_AUDIO: + self._audio_pixbuf = pixbuf + else: + self.model.save_photo(pixbuf) + + def record_video(self, quality): + if not self._has_camera: + return + + self._ogg_quality = quality + self._config_videobin(OGG_TRAITS[quality]['quality'], + OGG_TRAITS[quality]['width'], + OGG_TRAITS[quality]['height']) + + # If we use pad blocking and adjust the pipeline on-the-fly, the + # resultant video has bad A/V sync :( + # If we pause the pipeline while adjusting it, the A/V sync is better + # but not perfect :( + # so we stop the whole thing while reconfiguring to get the best results + self._pipeline.set_state(gst.STATE_NULL) + self._pipeline.add(self._videobin) + self._pipeline.get_by_name("tee").link(self._videobin) + self._pipeline.add(self._audiobin) + self.play() + + def record_audio(self): + if self._has_camera: + self._audio_pixbuf = None + self._take_photo(self.PHOTO_MODE_AUDIO) + + # we should be able to add the audiobin on the fly, but unfortunately + # this results in several seconds of silence being added at the start + # of the recording. So we stop the whole pipeline while adjusting it. + # SL#2040 + self._pipeline.set_state(gst.STATE_NULL) + self._pipeline.add(self._audiobin) + self.play() + + def stop_recording_video(self): + if not self._has_camera: + return + + # We stop the pipeline while we are adjusting the pipeline to stop + # recording because if we do it on-the-fly, the following video live + # feed to the screen becomes several seconds delayed. Weird! + # FIXME: retest on F11 + # FIXME: could this be the result of audio shortening problems? + self._eos_cb = self._video_eos + self._pipeline.get_by_name('camsrc').send_event(gst.event_new_eos()) + self._audiobin.get_by_name('absrc').send_event(gst.event_new_eos()) + + def _video_eos(self): + self._pipeline.set_state(gst.STATE_NULL) + self._pipeline.get_by_name("tee").unlink(self._videobin) + self._pipeline.remove(self._videobin) + self._pipeline.remove(self._audiobin) + + self.model.shutter_sound() + + if len(self._thumb_pipes) > 0: + thumbline = self._thumb_pipes[-1] + thumbline.get_by_name('thumb_fakesink').disconnect(self._thumb_handoff_handler) + + ogg_path = os.path.join(Instance.instancePath, "output.ogg") #ogv + if not os.path.exists(ogg_path) or os.path.getsize(ogg_path) <= 0: + # FIXME: inform model of failure? + return + + line = 'filesrc location=' + ogg_path + ' name=thumbFilesrc ! oggdemux name=thumbOggdemux ! theoradec name=thumbTheoradec ! tee name=thumb_tee ! queue name=thumb_queue ! ffmpegcolorspace name=thumbFfmpegcolorspace ! jpegenc name=thumbJPegenc ! fakesink name=thumb_fakesink' + thumbline = gst.parse_launch(line) + thumb_queue = thumbline.get_by_name('thumb_queue') + thumb_queue.set_property("leaky", True) + thumb_queue.set_property("max-size-buffers", 1) + thumb_tee = thumbline.get_by_name('thumb_tee') + thumb_fakesink = thumbline.get_by_name('thumb_fakesink') + self._thumb_handoff_handler = thumb_fakesink.connect("handoff", self.copyThumbPic) + thumb_fakesink.set_property("signal-handoffs", True) + self._thumb_pipes.append(thumbline) + self._thumb_exposure_open = True + thumbline.set_state(gst.STATE_PLAYING) + + def copyThumbPic(self, fsink, buffer, pad, user_data=None): + if not self._thumb_exposure_open: + return + + self._thumb_exposure_open = False + loader = gtk.gdk.pixbuf_loader_new_with_mime_type("image/jpeg") + loader.write(buffer) + loader.close() + self.thumbBuf = loader.get_pixbuf() + self.model.still_ready(self.thumbBuf) + + self._thumb_element('thumb_tee').unlink(self._thumb_element('thumb_queue')) + + oggFilepath = os.path.join(Instance.instancePath, "output.ogg") #ogv + wavFilepath = os.path.join(Instance.instancePath, "output.wav") + muxFilepath = os.path.join(Instance.instancePath, "mux.ogg") #ogv + + muxline = gst.parse_launch('filesrc location=' + str(oggFilepath) + ' name=muxVideoFilesrc ! oggdemux name=muxOggdemux ! theoraparse ! oggmux name=muxOggmux ! filesink location=' + str(muxFilepath) + ' name=muxFilesink filesrc location=' + str(wavFilepath) + ' name=muxAudioFilesrc ! wavparse name=muxWavparse ! audioconvert name=muxAudioconvert ! vorbisenc name=muxVorbisenc ! muxOggmux.') + taglist = self._get_tags(constants.TYPE_VIDEO) + vorbis_enc = muxline.get_by_name('muxVorbisenc') + vorbis_enc.merge_tags(taglist, gst.TAG_MERGE_REPLACE_ALL) + + muxBus = muxline.get_bus() + muxBus.add_signal_watch() + self._video_transcode_handler = muxBus.connect('message', self._onMuxedVideoMessageCb, muxline) + self._mux_pipes.append(muxline) + #add a listener here to monitor % of transcoding... + self._transcode_id = gobject.timeout_add(200, self._transcodeUpdateCb, muxline) + muxline.set_state(gst.STATE_PLAYING) + + def _transcodeUpdateCb( self, pipe ): + position, duration = self._query_position( pipe ) + if position != gst.CLOCK_TIME_NONE: + value = position * 100.0 / duration + value = value/100.0 + self.model.set_progress(value, _('Saving...')) + return True + + def _query_position(self, pipe): + try: + position, format = pipe.query_position(gst.FORMAT_TIME) + except: + position = gst.CLOCK_TIME_NONE + + try: + duration, format = pipe.query_duration(gst.FORMAT_TIME) + except: + duration = gst.CLOCK_TIME_NONE + + return (position, duration) + + def _onMuxedVideoMessageCb(self, bus, message, pipe): + if message.type != gst.MESSAGE_EOS: + return True + + gobject.source_remove(self._video_transcode_handler) + self._video_transcode_handler = None + gobject.source_remove(self._transcode_id) + self._transcode_id = None + pipe.set_state(gst.STATE_NULL) + pipe.get_bus().remove_signal_watch() + pipe.get_bus().disable_sync_message_emission() + + wavFilepath = os.path.join(Instance.instancePath, "output.wav") + oggFilepath = os.path.join(Instance.instancePath, "output.ogg") #ogv + muxFilepath = os.path.join(Instance.instancePath, "mux.ogg") #ogv + os.remove( wavFilepath ) + os.remove( oggFilepath ) + self.model.save_video(muxFilepath, self.thumbBuf) + return False + + def _onMuxedAudioMessageCb(self, bus, message, pipe): + if message.type != gst.MESSAGE_EOS: + return True + + gobject.source_remove(self._audio_transcode_handler) + self._audio_transcode_handler = None + gobject.source_remove(self._transcode_id) + self._transcode_id = None + pipe.set_state(gst.STATE_NULL) + pipe.get_bus().remove_signal_watch() + pipe.get_bus().disable_sync_message_emission() + + wavFilepath = os.path.join(Instance.instancePath, "output.wav") + oggFilepath = os.path.join(Instance.instancePath, "output.ogg") + os.remove( wavFilepath ) + self.model.save_audio(oggFilepath, self._audio_pixbuf) + return False + + def _bus_message_handler(self, bus, message): + t = message.type + if t == gst.MESSAGE_EOS: + if self._eos_cb: + cb = self._eos_cb + self._eos_cb = None + cb() + elif t == gst.MESSAGE_ERROR: + #todo: if we come out of suspend/resume with errors, then get us back up and running... + #todo: handle "No space left on the resource.gstfilesink.c" + #err, debug = message.parse_error() + pass + + def abandonMedia(self): + self.stop() + + if self._audio_transcode_handler: + gobject.source_remove(self._audio_transcode_handler) + self._audio_transcode_handler = None + if self._transcode_id: + gobject.source_remove(self._transcode_id) + self._transcode_id = None + if self._video_transcode_handler: + gobject.source_remove(self._video_transcode_handler) + self._video_transcode_handler = None + + wav_path = os.path.join(Instance.instancePath, "output.wav") + if os.path.exists(wav_path): + os.remove(wav_path) + ogg_path = os.path.join(Instance.instancePath, "output.ogg") #ogv + if os.path.exists(ogg_path): + os.remove(ogg_path) + mux_path = os.path.join(Instance.instancePath, "mux.ogg") #ogv + if os.path.exists(mux_path): + os.remove(mux_path) + diff --git a/plugins/camera_sensor/icons/sensoroff.svg b/plugins/camera_sensor/icons/sensoroff.svg new file mode 100644 index 0000000..0a16670 --- /dev/null +++ b/plugins/camera_sensor/icons/sensoroff.svg @@ -0,0 +1,79 @@ + + + +image/svg+xml + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/camera_sensor/icons/sensoron.svg b/plugins/camera_sensor/icons/sensoron.svg new file mode 100644 index 0000000..d756860 --- /dev/null +++ b/plugins/camera_sensor/icons/sensoron.svg @@ -0,0 +1,63 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/plugins/camera_sensor/images/camera1off.svg b/plugins/camera_sensor/images/camera1off.svg new file mode 100644 index 0000000..2abf8d2 --- /dev/null +++ b/plugins/camera_sensor/images/camera1off.svg @@ -0,0 +1,66 @@ + + + +image/svg+xml + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/camera_sensor/images/camera1small.svg b/plugins/camera_sensor/images/camera1small.svg new file mode 100644 index 0000000..4d7f756 --- /dev/null +++ b/plugins/camera_sensor/images/camera1small.svg @@ -0,0 +1,68 @@ + + + +image/svg+xml + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/camera_sensor/images/cameraoff.svg b/plugins/camera_sensor/images/cameraoff.svg new file mode 100644 index 0000000..54d9c86 --- /dev/null +++ b/plugins/camera_sensor/images/cameraoff.svg @@ -0,0 +1,15 @@ + + +]> + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/camera_sensor/images/camerasmall.svg b/plugins/camera_sensor/images/camerasmall.svg new file mode 100644 index 0000000..690e940 --- /dev/null +++ b/plugins/camera_sensor/images/camerasmall.svg @@ -0,0 +1,51 @@ + + + +image/svg+xml + + + + + + + + + + \ No newline at end of file diff --git a/plugins/camera_sensor/instance.py b/plugins/camera_sensor/instance.py new file mode 100644 index 0000000..bcee466 --- /dev/null +++ b/plugins/camera_sensor/instance.py @@ -0,0 +1,22 @@ +import os + +from sugar import profile +from sugar import util + +class Instance: + key = profile.get_pubkey() + keyHash = util.sha_data(key) + + keyHashPrintable = util.printable_hash(keyHash) + + instancePath = None + + def __init__(self, ca): + self.__class__.instancePath = os.path.join(ca.get_activity_root(), "instance") + recreateTmp() + + +def recreateTmp(): + if (not os.path.exists(Instance.instancePath)): + os.makedirs(Instance.instancePath) + diff --git a/plugins/camera_sensor/tacamera.py b/plugins/camera_sensor/tacamera.py new file mode 100644 index 0000000..fc0804d --- /dev/null +++ b/plugins/camera_sensor/tacamera.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +#Copyright (c) 2010, Walter Bender +#Copyright (c) 2010, Tony Forster + +#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 gst, time +import gobject + +from TurtleArt.tautils import debug_output + + +class Camera(): + ''' Sets up a pipe from the camera to a pixbuf and emits a signal + when the image is ready. ''' + + def __init__(self, device='/dev/video0'): + ''' Prepare camera pipeline to pixbuf and signal watch ''' + self.pipe = gst.Pipeline('pipeline') + v4l2src = gst.element_factory_make('v4l2src', None) + v4l2src.props.device = device + self.pipe.add(v4l2src) + ffmpegcolorspace = gst.element_factory_make('ffmpegcolorspace', None) + self.pipe.add(ffmpegcolorspace) + gdkpixbufsink = gst.element_factory_make('gdkpixbufsink', None) + self.pipe.add(gdkpixbufsink) + gst.element_link_many(v4l2src, ffmpegcolorspace, gdkpixbufsink) + if self.pipe is not None: + self.bus = self.pipe.get_bus() + self.bus.add_signal_watch() + self.bus.connect('message', self._on_message) + status = True + else: + status = False + + def _on_message(self, bus, message): + ''' We get a message if a pixbuf is available ''' + if message.structure is not None: + if message.structure.get_name() == 'pixbuf': + self.pixbuf = message.structure['pixbuf'] + self.image_ready = True + + def start_camera_input(self): + ''' Start grabbing ''' + self.pixbuf = None + self.image_ready = False + self.pipe.set_state(gst.STATE_PLAYING) + while not self.image_ready: + self.bus.poll(gst.MESSAGE_ANY, -1) + + def stop_camera_input(self): + ''' Stop grabbing ''' + self.pipe.set_state(gst.STATE_NULL) diff --git a/plugins/camera_sensor/utils.py b/plugins/camera_sensor/utils.py new file mode 100644 index 0000000..701e45f --- /dev/null +++ b/plugins/camera_sensor/utils.py @@ -0,0 +1,63 @@ +import base64 +import rsvg +import re +import os +import gtk +import time +from time import strftime + +import constants + + +def getStringEncodedFromPixbuf(pixbuf): + data = [""] + pixbuf.save_to_callback(_saveDataToBufferCb, "png", {}, data) + return base64.b64encode(str(data[0])) + + +def getStringFromPixbuf(pixbuf): + data = [""] + pixbuf.save_to_callback(_saveDataToBufferCb, "png", {}, data) + return str(data[0]) + + +def _saveDataToBufferCb(buf, data): + data[0] += buf + return True + + +def getPixbufFromString(str): + pbl = gtk.gdk.PixbufLoader() + data = base64.b64decode( str ) + pbl.write(data) + pbl.close() + return pbl.get_pixbuf() + + +def load_colored_svg(filename, stroke, fill): + path = os.path.join(constants.GFX_PATH, filename) + data = open(path, 'r').read() + + entity = '' % fill + data = re.sub('', entity, data) + + entity = '' % stroke + data = re.sub('', entity, data) + + return rsvg.Handle(data=data).get_pixbuf() + +def getUniqueFilepath( path, i ): + pathOb = os.path.abspath( path ) + newPath = os.path.join( os.path.dirname(pathOb), str( str(i) + os.path.basename(pathOb) ) ) + if (os.path.exists(newPath)): + i = i + 1 + return getUniqueFilepath( pathOb, i ) + else: + return os.path.abspath( newPath ) + +def generate_thumbnail(pixbuf): + return pixbuf.scale_simple(108, 81, gtk.gdk.INTERP_BILINEAR) + +def getDateString( when ): + return strftime( "%c", time.localtime(when) ) + diff --git a/plugins/camera_sensor/v4l2.py b/plugins/camera_sensor/v4l2.py new file mode 100644 index 0000000..9c052fd --- /dev/null +++ b/plugins/camera_sensor/v4l2.py @@ -0,0 +1,1914 @@ +# Python bindings for the v4l2 userspace api + +# Copyright (C) 1999-2009 the contributors + +# 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 2 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# Alternatively you can redistribute this file under the terms of the +# BSD license as stated below: + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# 3. The names of its contributors may not be used to endorse or promote +# products derived from this software without specific prior written +# permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +Python bindings for the v4l2 userspace api in Linux 2.6.34 +""" + +# see linux/videodev2.h + +import ctypes + + +_IOC_NRBITS = 8 +_IOC_TYPEBITS = 8 +_IOC_SIZEBITS = 14 +_IOC_DIRBITS = 2 + +_IOC_NRSHIFT = 0 +_IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS +_IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS +_IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS + +_IOC_NONE = 0 +_IOC_WRITE = 1 +_IOC_READ = 2 + + +def _IOC(dir_, type_, nr, size): + return ( + ctypes.c_int32(dir_ << _IOC_DIRSHIFT).value | + ctypes.c_int32(ord(type_) << _IOC_TYPESHIFT).value | + ctypes.c_int32(nr << _IOC_NRSHIFT).value | + ctypes.c_int32(size << _IOC_SIZESHIFT).value) + + +def _IOC_TYPECHECK(t): + return ctypes.sizeof(t) + + +def _IO(type_, nr): + return _IOC(_IOC_NONE, type_, nr, 0) + + +def _IOW(type_, nr, size): + return _IOC(_IOC_WRITE, type_, nr, _IOC_TYPECHECK(size)) + + +def _IOR(type_, nr, size): + return _IOC(_IOC_READ, type_, nr, _IOC_TYPECHECK(size)) + + +def _IOWR(type_, nr, size): + return _IOC(_IOC_READ | _IOC_WRITE, type_, nr, _IOC_TYPECHECK(size)) + + +# +# type alias +# + +enum = ctypes.c_uint +c_int = ctypes.c_int + + +# +# time +# + +class timeval(ctypes.Structure): + _fields_ = [ + ('secs', ctypes.c_long), + ('usecs', ctypes.c_long), + ] + + +# +# v4l2 +# + + +VIDEO_MAX_FRAME = 32 + + +VID_TYPE_CAPTURE = 1 +VID_TYPE_TUNER = 2 +VID_TYPE_TELETEXT = 4 +VID_TYPE_OVERLAY = 8 +VID_TYPE_CHROMAKEY = 16 +VID_TYPE_CLIPPING = 32 +VID_TYPE_FRAMERAM = 64 +VID_TYPE_SCALES = 128 +VID_TYPE_MONOCHROME = 256 +VID_TYPE_SUBCAPTURE = 512 +VID_TYPE_MPEG_DECODER = 1024 +VID_TYPE_MPEG_ENCODER = 2048 +VID_TYPE_MJPEG_DECODER = 4096 +VID_TYPE_MJPEG_ENCODER = 8192 + + +def v4l2_fourcc(a, b, c, d): + return ord(a) | (ord(b) << 8) | (ord(c) << 16) | (ord(d) << 24) + + +v4l2_field = enum +( + V4L2_FIELD_ANY, + V4L2_FIELD_NONE, + V4L2_FIELD_TOP, + V4L2_FIELD_BOTTOM, + V4L2_FIELD_INTERLACED, + V4L2_FIELD_SEQ_TB, + V4L2_FIELD_SEQ_BT, + V4L2_FIELD_ALTERNATE, + V4L2_FIELD_INTERLACED_TB, + V4L2_FIELD_INTERLACED_BT, +) = range(10) + + +def V4L2_FIELD_HAS_TOP(field): + return ( + field == V4L2_FIELD_TOP or + field == V4L2_FIELD_INTERLACED or + field == V4L2_FIELD_INTERLACED_TB or + field == V4L2_FIELD_INTERLACED_BT or + field == V4L2_FIELD_SEQ_TB or + field == V4L2_FIELD_SEQ_BT) + + +def V4L2_FIELD_HAS_BOTTOM(field): + return ( + field == V4L2_FIELD_BOTTOM or + field == V4L2_FIELD_INTERLACED or + field == V4L2_FIELD_INTERLACED_TB or + field == V4L2_FIELD_INTERLACED_BT or + field == V4L2_FIELD_SEQ_TB or + field == V4L2_FIELD_SEQ_BT) + + +def V4L2_FIELD_HAS_BOTH(field): + return ( + field == V4L2_FIELD_INTERLACED or + field == V4L2_FIELD_INTERLACED_TB or + field == V4L2_FIELD_INTERLACED_BT or + field == V4L2_FIELD_SEQ_TB or + field == V4L2_FIELD_SEQ_BT) + + +v4l2_buf_type = enum +( + V4L2_BUF_TYPE_VIDEO_CAPTURE, + V4L2_BUF_TYPE_VIDEO_OUTPUT, + V4L2_BUF_TYPE_VIDEO_OVERLAY, + V4L2_BUF_TYPE_VBI_CAPTURE, + V4L2_BUF_TYPE_VBI_OUTPUT, + V4L2_BUF_TYPE_SLICED_VBI_CAPTURE, + V4L2_BUF_TYPE_SLICED_VBI_OUTPUT, + V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY, + V4L2_BUF_TYPE_PRIVATE, +) = range(1, 9) + [0x80] + + +v4l2_ctrl_type = enum +( + V4L2_CTRL_TYPE_INTEGER, + V4L2_CTRL_TYPE_BOOLEAN, + V4L2_CTRL_TYPE_MENU, + V4L2_CTRL_TYPE_BUTTON, + V4L2_CTRL_TYPE_INTEGER64, + V4L2_CTRL_TYPE_CTRL_CLASS, + V4L2_CTRL_TYPE_STRING, +) = range(1, 8) + + +v4l2_tuner_type = enum +( + V4L2_TUNER_RADIO, + V4L2_TUNER_ANALOG_TV, + V4L2_TUNER_DIGITAL_TV, +) = range(1, 4) + + +v4l2_memory = enum +( + V4L2_MEMORY_MMAP, + V4L2_MEMORY_USERPTR, + V4L2_MEMORY_OVERLAY, +) = range(1, 4) + + +v4l2_colorspace = enum +( + V4L2_COLORSPACE_SMPTE170M, + V4L2_COLORSPACE_SMPTE240M, + V4L2_COLORSPACE_REC709, + V4L2_COLORSPACE_BT878, + V4L2_COLORSPACE_470_SYSTEM_M, + V4L2_COLORSPACE_470_SYSTEM_BG, + V4L2_COLORSPACE_JPEG, + V4L2_COLORSPACE_SRGB, +) = range(1, 9) + + +v4l2_priority = enum +( + V4L2_PRIORITY_UNSET, + V4L2_PRIORITY_BACKGROUND, + V4L2_PRIORITY_INTERACTIVE, + V4L2_PRIORITY_RECORD, + V4L2_PRIORITY_DEFAULT, +) = range(0, 4) + [2] + + +class v4l2_rect(ctypes.Structure): + _fields_ = [ + ('left', ctypes.c_int32), + ('top', ctypes.c_int32), + ('width', ctypes.c_int32), + ('height', ctypes.c_int32), + ] + + +class v4l2_fract(ctypes.Structure): + _fields_ = [ + ('numerator', ctypes.c_uint32), + ('denominator', ctypes.c_uint32), + ] + + +# +# Driver capabilities +# + +class v4l2_capability(ctypes.Structure): + _fields_ = [ + ('driver', ctypes.c_char * 16), + ('card', ctypes.c_char * 32), + ('bus_info', ctypes.c_char * 32), + ('version', ctypes.c_uint32), + ('capabilities', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ] + + +# +# Values for 'capabilities' field +# + +V4L2_CAP_VIDEO_CAPTURE = 0x00000001 +V4L2_CAP_VIDEO_OUTPUT = 0x00000002 +V4L2_CAP_VIDEO_OVERLAY = 0x00000004 +V4L2_CAP_VBI_CAPTURE = 0x00000010 +V4L2_CAP_VBI_OUTPUT = 0x00000020 +V4L2_CAP_SLICED_VBI_CAPTURE = 0x00000040 +V4L2_CAP_SLICED_VBI_OUTPUT = 0x00000080 +V4L2_CAP_RDS_CAPTURE = 0x00000100 +V4L2_CAP_VIDEO_OUTPUT_OVERLAY = 0x00000200 +V4L2_CAP_HW_FREQ_SEEK = 0x00000400 +V4L2_CAP_RDS_OUTPUT = 0x00000800 + +V4L2_CAP_TUNER = 0x00010000 +V4L2_CAP_AUDIO = 0x00020000 +V4L2_CAP_RADIO = 0x00040000 +V4L2_CAP_MODULATOR = 0x00080000 + +V4L2_CAP_READWRITE = 0x01000000 +V4L2_CAP_ASYNCIO = 0x02000000 +V4L2_CAP_STREAMING = 0x04000000 + + +# +# Video image format +# + +class v4l2_pix_format(ctypes.Structure): + _fields_ = [ + ('width', ctypes.c_uint32), + ('height', ctypes.c_uint32), + ('pixelformat', ctypes.c_uint32), + ('field', v4l2_field), + ('bytesperline', ctypes.c_uint32), + ('sizeimage', ctypes.c_uint32), + ('colorspace', v4l2_colorspace), + ('priv', ctypes.c_uint32), + ] + +# RGB formats +V4L2_PIX_FMT_RGB332 = v4l2_fourcc('R', 'G', 'B', '1') +V4L2_PIX_FMT_RGB444 = v4l2_fourcc('R', '4', '4', '4') +V4L2_PIX_FMT_RGB555 = v4l2_fourcc('R', 'G', 'B', 'O') +V4L2_PIX_FMT_RGB565 = v4l2_fourcc('R', 'G', 'B', 'P') +V4L2_PIX_FMT_RGB555X = v4l2_fourcc('R', 'G', 'B', 'Q') +V4L2_PIX_FMT_RGB565X = v4l2_fourcc('R', 'G', 'B', 'R') +V4L2_PIX_FMT_BGR24 = v4l2_fourcc('B', 'G', 'R', '3') +V4L2_PIX_FMT_RGB24 = v4l2_fourcc('R', 'G', 'B', '3') +V4L2_PIX_FMT_BGR32 = v4l2_fourcc('B', 'G', 'R', '4') +V4L2_PIX_FMT_RGB32 = v4l2_fourcc('R', 'G', 'B', '4') + +# Grey formats +V4L2_PIX_FMT_GREY = v4l2_fourcc('G', 'R', 'E', 'Y') +V4L2_PIX_FMT_Y10 = v4l2_fourcc('Y', '1', '0', ' ') +V4L2_PIX_FMT_Y16 = v4l2_fourcc('Y', '1', '6', ' ') + +# Palette formats +V4L2_PIX_FMT_PAL8 = v4l2_fourcc('P', 'A', 'L', '8') + +# Luminance+Chrominance formats +V4L2_PIX_FMT_YVU410 = v4l2_fourcc('Y', 'V', 'U', '9') +V4L2_PIX_FMT_YVU420 = v4l2_fourcc('Y', 'V', '1', '2') +V4L2_PIX_FMT_YUYV = v4l2_fourcc('Y', 'U', 'Y', 'V') +V4L2_PIX_FMT_YYUV = v4l2_fourcc('Y', 'Y', 'U', 'V') +V4L2_PIX_FMT_YVYU = v4l2_fourcc('Y', 'V', 'Y', 'U') +V4L2_PIX_FMT_UYVY = v4l2_fourcc('U', 'Y', 'V', 'Y') +V4L2_PIX_FMT_VYUY = v4l2_fourcc('V', 'Y', 'U', 'Y') +V4L2_PIX_FMT_YUV422P = v4l2_fourcc('4', '2', '2', 'P') +V4L2_PIX_FMT_YUV411P = v4l2_fourcc('4', '1', '1', 'P') +V4L2_PIX_FMT_Y41P = v4l2_fourcc('Y', '4', '1', 'P') +V4L2_PIX_FMT_YUV444 = v4l2_fourcc('Y', '4', '4', '4') +V4L2_PIX_FMT_YUV555 = v4l2_fourcc('Y', 'U', 'V', 'O') +V4L2_PIX_FMT_YUV565 = v4l2_fourcc('Y', 'U', 'V', 'P') +V4L2_PIX_FMT_YUV32 = v4l2_fourcc('Y', 'U', 'V', '4') +V4L2_PIX_FMT_YUV410 = v4l2_fourcc('Y', 'U', 'V', '9') +V4L2_PIX_FMT_YUV420 = v4l2_fourcc('Y', 'U', '1', '2') +V4L2_PIX_FMT_HI240 = v4l2_fourcc('H', 'I', '2', '4') +V4L2_PIX_FMT_HM12 = v4l2_fourcc('H', 'M', '1', '2') + +# two planes -- one Y, one Cr + Cb interleaved +V4L2_PIX_FMT_NV12 = v4l2_fourcc('N', 'V', '1', '2') +V4L2_PIX_FMT_NV21 = v4l2_fourcc('N', 'V', '2', '1') +V4L2_PIX_FMT_NV16 = v4l2_fourcc('N', 'V', '1', '6') +V4L2_PIX_FMT_NV61 = v4l2_fourcc('N', 'V', '6', '1') + +# Bayer formats - see http://www.siliconimaging.com/RGB%20Bayer.htm +V4L2_PIX_FMT_SBGGR8 = v4l2_fourcc('B', 'A', '8', '1') +V4L2_PIX_FMT_SGBRG8 = v4l2_fourcc('G', 'B', 'R', 'G') +V4L2_PIX_FMT_SGRBG8 = v4l2_fourcc('G', 'R', 'B', 'G') +V4L2_PIX_FMT_SRGGB8 = v4l2_fourcc('R', 'G', 'G', 'B') +V4L2_PIX_FMT_SBGGR10 = v4l2_fourcc('B', 'G', '1', '0') +V4L2_PIX_FMT_SGBRG10 = v4l2_fourcc('G', 'B', '1', '0') +V4L2_PIX_FMT_SGRBG10 = v4l2_fourcc('B', 'A', '1', '0') +V4L2_PIX_FMT_SRGGB10 = v4l2_fourcc('R', 'G', '1', '0') +V4L2_PIX_FMT_SGRBG10DPCM8 = v4l2_fourcc('B', 'D', '1', '0') +V4L2_PIX_FMT_SBGGR16 = v4l2_fourcc('B', 'Y', 'R', '2') + +# compressed formats +V4L2_PIX_FMT_MJPEG = v4l2_fourcc('M', 'J', 'P', 'G') +V4L2_PIX_FMT_JPEG = v4l2_fourcc('J', 'P', 'E', 'G') +V4L2_PIX_FMT_DV = v4l2_fourcc('d', 'v', 's', 'd') +V4L2_PIX_FMT_MPEG = v4l2_fourcc('M', 'P', 'E', 'G') + +# Vendor-specific formats +V4L2_PIX_FMT_CPIA1 = v4l2_fourcc('C', 'P', 'I', 'A') +V4L2_PIX_FMT_WNVA = v4l2_fourcc('W', 'N', 'V', 'A') +V4L2_PIX_FMT_SN9C10X = v4l2_fourcc('S', '9', '1', '0') +V4L2_PIX_FMT_SN9C20X_I420 = v4l2_fourcc('S', '9', '2', '0') +V4L2_PIX_FMT_PWC1 = v4l2_fourcc('P', 'W', 'C', '1') +V4L2_PIX_FMT_PWC2 = v4l2_fourcc('P', 'W', 'C', '2') +V4L2_PIX_FMT_ET61X251 = v4l2_fourcc('E', '6', '2', '5') +V4L2_PIX_FMT_SPCA501 = v4l2_fourcc('S', '5', '0', '1') +V4L2_PIX_FMT_SPCA505 = v4l2_fourcc('S', '5', '0', '5') +V4L2_PIX_FMT_SPCA508 = v4l2_fourcc('S', '5', '0', '8') +V4L2_PIX_FMT_SPCA561 = v4l2_fourcc('S', '5', '6', '1') +V4L2_PIX_FMT_PAC207 = v4l2_fourcc('P', '2', '0', '7') +V4L2_PIX_FMT_MR97310A = v4l2_fourcc('M', '3', '1', '0') +V4L2_PIX_FMT_SN9C2028 = v4l2_fourcc('S', 'O', 'N', 'X') +V4L2_PIX_FMT_SQ905C = v4l2_fourcc('9', '0', '5', 'C') +V4L2_PIX_FMT_PJPG = v4l2_fourcc('P', 'J', 'P', 'G') +V4L2_PIX_FMT_OV511 = v4l2_fourcc('O', '5', '1', '1') +V4L2_PIX_FMT_OV518 = v4l2_fourcc('O', '5', '1', '8') +V4L2_PIX_FMT_STV0680 = v4l2_fourcc('S', '6', '8', '0') + + +# +# Format enumeration +# + +class v4l2_fmtdesc(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('type', ctypes.c_int), + ('flags', ctypes.c_uint32), + ('description', ctypes.c_char * 32), + ('pixelformat', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ] + +V4L2_FMT_FLAG_COMPRESSED = 0x0001 +V4L2_FMT_FLAG_EMULATED = 0x0002 + + +# +# Experimental frame size and frame rate enumeration +# + +v4l2_frmsizetypes = enum +( + V4L2_FRMSIZE_TYPE_DISCRETE, + V4L2_FRMSIZE_TYPE_CONTINUOUS, + V4L2_FRMSIZE_TYPE_STEPWISE, +) = range(1, 4) + + +class v4l2_frmsize_discrete(ctypes.Structure): + _fields_ = [ + ('width', ctypes.c_uint32), + ('height', ctypes.c_uint32), + ] + + +class v4l2_frmsize_stepwise(ctypes.Structure): + _fields_ = [ + ('min_width', ctypes.c_uint32), + ('min_height', ctypes.c_uint32), + ('step_width', ctypes.c_uint32), + ('min_height', ctypes.c_uint32), + ('max_height', ctypes.c_uint32), + ('step_height', ctypes.c_uint32), + ] + + +class v4l2_frmsizeenum(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('discrete', v4l2_frmsize_discrete), + ('stepwise', v4l2_frmsize_stepwise), + ] + + _fields_ = [ + ('index', ctypes.c_uint32), + ('pixel_format', ctypes.c_uint32), + ('type', ctypes.c_uint32), + ('_u', _u), + ('reserved', ctypes.c_uint32 * 2) + ] + + _anonymous_ = ('_u',) + + +# +# Frame rate enumeration +# + +v4l2_frmivaltypes = enum +( + V4L2_FRMIVAL_TYPE_DISCRETE, + V4L2_FRMIVAL_TYPE_CONTINUOUS, + V4L2_FRMIVAL_TYPE_STEPWISE, +) = range(1, 4) + + +class v4l2_frmival_stepwise(ctypes.Structure): + _fields_ = [ + ('min', v4l2_fract), + ('max', v4l2_fract), + ('step', v4l2_fract), + ] + + +class v4l2_frmivalenum(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('discrete', v4l2_fract), + ('stepwise', v4l2_frmival_stepwise), + ] + + _fields_ = [ + ('index', ctypes.c_uint32), + ('pixel_format', ctypes.c_uint32), + ('width', ctypes.c_uint32), + ('height', ctypes.c_uint32), + ('type', ctypes.c_uint32), + ('_u', _u), + ('reserved', ctypes.c_uint32 * 2), + ] + + _anonymous_ = ('_u',) + + +# +# Timecode +# + +class v4l2_timecode(ctypes.Structure): + _fields_ = [ + ('type', ctypes.c_uint32), + ('flags', ctypes.c_uint32), + ('frames', ctypes.c_uint8), + ('seconds', ctypes.c_uint8), + ('minutes', ctypes.c_uint8), + ('hours', ctypes.c_uint8), + ('userbits', ctypes.c_uint8 * 4), + ] + + +V4L2_TC_TYPE_24FPS = 1 +V4L2_TC_TYPE_25FPS = 2 +V4L2_TC_TYPE_30FPS = 3 +V4L2_TC_TYPE_50FPS = 4 +V4L2_TC_TYPE_60FPS = 5 + +V4L2_TC_FLAG_DROPFRAME = 0x0001 +V4L2_TC_FLAG_COLORFRAME = 0x0002 +V4L2_TC_USERBITS_field = 0x000C +V4L2_TC_USERBITS_USERDEFINED = 0x0000 +V4L2_TC_USERBITS_8BITCHARS = 0x0008 + + +class v4l2_jpegcompression(ctypes.Structure): + _fields_ = [ + ('quality', ctypes.c_int), + ('APPn', ctypes.c_int), + ('APP_len', ctypes.c_int), + ('APP_data', ctypes.c_char * 60), + ('COM_len', ctypes.c_int), + ('COM_data', ctypes.c_char * 60), + ('jpeg_markers', ctypes.c_uint32), + ] + + +V4L2_JPEG_MARKER_DHT = 1 << 3 +V4L2_JPEG_MARKER_DQT = 1 << 4 +V4L2_JPEG_MARKER_DRI = 1 << 5 +V4L2_JPEG_MARKER_COM = 1 << 6 +V4L2_JPEG_MARKER_APP = 1 << 7 + + +# +# Memory-mapping buffers +# + +class v4l2_requestbuffers(ctypes.Structure): + _fields_ = [ + ('count', ctypes.c_uint32), + ('type', v4l2_buf_type), + ('memory', v4l2_memory), + ('reserved', ctypes.c_uint32 * 2), + ] + + +class v4l2_buffer(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('offset', ctypes.c_uint32), + ('userptr', ctypes.c_ulong), + ] + + _fields_ = [ + ('index', ctypes.c_uint32), + ('type', v4l2_buf_type), + ('bytesused', ctypes.c_uint32), + ('flags', ctypes.c_uint32), + ('field', v4l2_field), + ('timestamp', timeval), + ('timecode', v4l2_timecode), + ('sequence', ctypes.c_uint32), + ('memory', v4l2_memory), + ('m', _u), + ('length', ctypes.c_uint32), + ('input', ctypes.c_uint32), + ('reserved', ctypes.c_uint32), + ] + + +V4L2_BUF_FLAG_MAPPED = 0x0001 +V4L2_BUF_FLAG_QUEUED = 0x0002 +V4L2_BUF_FLAG_DONE = 0x0004 +V4L2_BUF_FLAG_KEYFRAME = 0x0008 +V4L2_BUF_FLAG_PFRAME = 0x0010 +V4L2_BUF_FLAG_BFRAME = 0x0020 +V4L2_BUF_FLAG_TIMECODE = 0x0100 +V4L2_BUF_FLAG_INPUT = 0x0200 + + +# +# Overlay preview +# + +class v4l2_framebuffer(ctypes.Structure): + _fields_ = [ + ('capability', ctypes.c_uint32), + ('flags', ctypes.c_uint32), + ('base', ctypes.c_void_p), + ('fmt', v4l2_pix_format), + ] + +V4L2_FBUF_CAP_EXTERNOVERLAY = 0x0001 +V4L2_FBUF_CAP_CHROMAKEY = 0x0002 +V4L2_FBUF_CAP_LIST_CLIPPING = 0x0004 +V4L2_FBUF_CAP_BITMAP_CLIPPING = 0x0008 +V4L2_FBUF_CAP_LOCAL_ALPHA = 0x0010 +V4L2_FBUF_CAP_GLOBAL_ALPHA = 0x0020 +V4L2_FBUF_CAP_LOCAL_INV_ALPHA = 0x0040 +V4L2_FBUF_CAP_SRC_CHROMAKEY = 0x0080 + +V4L2_FBUF_FLAG_PRIMARY = 0x0001 +V4L2_FBUF_FLAG_OVERLAY = 0x0002 +V4L2_FBUF_FLAG_CHROMAKEY = 0x0004 +V4L2_FBUF_FLAG_LOCAL_ALPHA = 0x0008 +V4L2_FBUF_FLAG_GLOBAL_ALPHA = 0x0010 +V4L2_FBUF_FLAG_LOCAL_INV_ALPHA = 0x0020 +V4L2_FBUF_FLAG_SRC_CHROMAKEY = 0x0040 + + +class v4l2_clip(ctypes.Structure): + pass +v4l2_clip._fields_ = [ + ('c', v4l2_rect), + ('next', ctypes.POINTER(v4l2_clip)), +] + + +class v4l2_window(ctypes.Structure): + _fields_ = [ + ('w', v4l2_rect), + ('field', v4l2_field), + ('chromakey', ctypes.c_uint32), + ('clips', ctypes.POINTER(v4l2_clip)), + ('clipcount', ctypes.c_uint32), + ('bitmap', ctypes.c_void_p), + ('global_alpha', ctypes.c_uint8), + ] + + +# +# Capture parameters +# + +class v4l2_captureparm(ctypes.Structure): + _fields_ = [ + ('capability', ctypes.c_uint32), + ('capturemode', ctypes.c_uint32), + ('timeperframe', v4l2_fract), + ('extendedmode', ctypes.c_uint32), + ('readbuffers', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ] + + +V4L2_MODE_HIGHQUALITY = 0x0001 +V4L2_CAP_TIMEPERFRAME = 0x1000 + + +class v4l2_outputparm(ctypes.Structure): + _fields_ = [ + ('capability', ctypes.c_uint32), + ('outputmode', ctypes.c_uint32), + ('timeperframe', v4l2_fract), + ('extendedmode', ctypes.c_uint32), + ('writebuffers', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ] + + +# +# Input image cropping +# + +class v4l2_cropcap(ctypes.Structure): + _fields_ = [ + ('type', v4l2_buf_type), + ('bounds', v4l2_rect), + ('defrect', v4l2_rect), + ('pixelaspect', v4l2_fract), + ] + + +class v4l2_crop(ctypes.Structure): + _fields_ = [ + ('type', ctypes.c_int), + ('c', v4l2_rect), + ] + + +# +# Analog video standard +# + +v4l2_std_id = ctypes.c_uint64 + + +V4L2_STD_PAL_B = 0x00000001 +V4L2_STD_PAL_B1 = 0x00000002 +V4L2_STD_PAL_G = 0x00000004 +V4L2_STD_PAL_H = 0x00000008 +V4L2_STD_PAL_I = 0x00000010 +V4L2_STD_PAL_D = 0x00000020 +V4L2_STD_PAL_D1 = 0x00000040 +V4L2_STD_PAL_K = 0x00000080 + +V4L2_STD_PAL_M = 0x00000100 +V4L2_STD_PAL_N = 0x00000200 +V4L2_STD_PAL_Nc = 0x00000400 +V4L2_STD_PAL_60 = 0x00000800 + +V4L2_STD_NTSC_M = 0x00001000 +V4L2_STD_NTSC_M_JP = 0x00002000 +V4L2_STD_NTSC_443 = 0x00004000 +V4L2_STD_NTSC_M_KR = 0x00008000 + +V4L2_STD_SECAM_B = 0x00010000 +V4L2_STD_SECAM_D = 0x00020000 +V4L2_STD_SECAM_G = 0x00040000 +V4L2_STD_SECAM_H = 0x00080000 +V4L2_STD_SECAM_K = 0x00100000 +V4L2_STD_SECAM_K1 = 0x00200000 +V4L2_STD_SECAM_L = 0x00400000 +V4L2_STD_SECAM_LC = 0x00800000 + +V4L2_STD_ATSC_8_VSB = 0x01000000 +V4L2_STD_ATSC_16_VSB = 0x02000000 + + +# some common needed stuff +V4L2_STD_PAL_BG = (V4L2_STD_PAL_B | V4L2_STD_PAL_B1 | V4L2_STD_PAL_G) +V4L2_STD_PAL_DK = (V4L2_STD_PAL_D | V4L2_STD_PAL_D1 | V4L2_STD_PAL_K) +V4L2_STD_PAL = (V4L2_STD_PAL_BG | V4L2_STD_PAL_DK | V4L2_STD_PAL_H | V4L2_STD_PAL_I) +V4L2_STD_NTSC = (V4L2_STD_NTSC_M | V4L2_STD_NTSC_M_JP | V4L2_STD_NTSC_M_KR) +V4L2_STD_SECAM_DK = (V4L2_STD_SECAM_D | V4L2_STD_SECAM_K | V4L2_STD_SECAM_K1) +V4L2_STD_SECAM = (V4L2_STD_SECAM_B | V4L2_STD_SECAM_G | V4L2_STD_SECAM_H | V4L2_STD_SECAM_DK | V4L2_STD_SECAM_L | V4L2_STD_SECAM_LC) + +V4L2_STD_525_60 = (V4L2_STD_PAL_M | V4L2_STD_PAL_60 | V4L2_STD_NTSC | V4L2_STD_NTSC_443) +V4L2_STD_625_50 = (V4L2_STD_PAL | V4L2_STD_PAL_N | V4L2_STD_PAL_Nc | V4L2_STD_SECAM) +V4L2_STD_ATSC = (V4L2_STD_ATSC_8_VSB | V4L2_STD_ATSC_16_VSB) + +V4L2_STD_UNKNOWN = 0 +V4L2_STD_ALL = (V4L2_STD_525_60 | V4L2_STD_625_50) + +# some merged standards +V4L2_STD_MN = (V4L2_STD_PAL_M | V4L2_STD_PAL_N | V4L2_STD_PAL_Nc | V4L2_STD_NTSC) +V4L2_STD_B = (V4L2_STD_PAL_B | V4L2_STD_PAL_B1 | V4L2_STD_SECAM_B) +V4L2_STD_GH = (V4L2_STD_PAL_G | V4L2_STD_PAL_H|V4L2_STD_SECAM_G | V4L2_STD_SECAM_H) +V4L2_STD_DK = (V4L2_STD_PAL_DK | V4L2_STD_SECAM_DK) + + +class v4l2_standard(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('id', v4l2_std_id), + ('name', ctypes.c_char * 24), + ('frameperiod', v4l2_fract), + ('framelines', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ] + + +# +# Video timings dv preset +# + +class v4l2_dv_preset(ctypes.Structure): + _fields_ = [ + ('preset', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4) + ] + + +# +# DV preset enumeration +# + +class v4l2_dv_enum_preset(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('preset', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('width', ctypes.c_uint32), + ('height', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ] + +# +# DV preset values +# + +V4L2_DV_INVALID = 0 +V4L2_DV_480P59_94 = 1 +V4L2_DV_576P50 = 2 +V4L2_DV_720P24 = 3 +V4L2_DV_720P25 = 4 +V4L2_DV_720P30 = 5 +V4L2_DV_720P50 = 6 +V4L2_DV_720P59_94 = 7 +V4L2_DV_720P60 = 8 +V4L2_DV_1080I29_97 = 9 +V4L2_DV_1080I30 = 10 +V4L2_DV_1080I25 = 11 +V4L2_DV_1080I50 = 12 +V4L2_DV_1080I60 = 13 +V4L2_DV_1080P24 = 14 +V4L2_DV_1080P25 = 15 +V4L2_DV_1080P30 = 16 +V4L2_DV_1080P50 = 17 +V4L2_DV_1080P60 = 18 + + +# +# DV BT timings +# + +class v4l2_bt_timings(ctypes.Structure): + _fields_ = [ + ('width', ctypes.c_uint32), + ('height', ctypes.c_uint32), + ('interlaced', ctypes.c_uint32), + ('polarities', ctypes.c_uint32), + ('pixelclock', ctypes.c_uint64), + ('hfrontporch', ctypes.c_uint32), + ('hsync', ctypes.c_uint32), + ('hbackporch', ctypes.c_uint32), + ('vfrontporch', ctypes.c_uint32), + ('vsync', ctypes.c_uint32), + ('vbackporch', ctypes.c_uint32), + ('il_vfrontporch', ctypes.c_uint32), + ('il_vsync', ctypes.c_uint32), + ('il_vbackporch', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 16), + ] + + _pack_ = True + +# Interlaced or progressive format +V4L2_DV_PROGRESSIVE = 0 +V4L2_DV_INTERLACED = 1 + +# Polarities. If bit is not set, it is assumed to be negative polarity +V4L2_DV_VSYNC_POS_POL = 0x00000001 +V4L2_DV_HSYNC_POS_POL = 0x00000002 + + +class v4l2_dv_timings(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('bt', v4l2_bt_timings), + ('reserved', ctypes.c_uint32 * 32), + ] + + _fields_ = [ + ('type', ctypes.c_uint32), + ('_u', _u), + ] + + _anonymous_ = ('_u',) + _pack_ = True + + +# Values for the type field +V4L2_DV_BT_656_1120 = 0 + + +# +# Video inputs +# + +class v4l2_input(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('type', ctypes.c_uint32), + ('audioset', ctypes.c_uint32), + ('tuner', ctypes.c_uint32), + ('std', v4l2_std_id), + ('status', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ] + + +V4L2_INPUT_TYPE_TUNER = 1 +V4L2_INPUT_TYPE_CAMERA = 2 + +V4L2_IN_ST_NO_POWER = 0x00000001 +V4L2_IN_ST_NO_SIGNAL = 0x00000002 +V4L2_IN_ST_NO_COLOR = 0x00000004 + +V4L2_IN_ST_HFLIP = 0x00000010 +V4L2_IN_ST_VFLIP = 0x00000020 + +V4L2_IN_ST_NO_H_LOCK = 0x00000100 +V4L2_IN_ST_COLOR_KILL = 0x00000200 + +V4L2_IN_ST_NO_SYNC = 0x00010000 +V4L2_IN_ST_NO_EQU = 0x00020000 +V4L2_IN_ST_NO_CARRIER = 0x00040000 + +V4L2_IN_ST_MACROVISION = 0x01000000 +V4L2_IN_ST_NO_ACCESS = 0x02000000 +V4L2_IN_ST_VTR = 0x04000000 + +V4L2_IN_CAP_PRESETS = 0x00000001 +V4L2_IN_CAP_CUSTOM_TIMINGS = 0x00000002 +V4L2_IN_CAP_STD = 0x00000004 + +# +# Video outputs +# + +class v4l2_output(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('type', ctypes.c_uint32), + ('audioset', ctypes.c_uint32), + ('modulator', ctypes.c_uint32), + ('std', v4l2_std_id), + ('reserved', ctypes.c_uint32 * 4), + ] + + +V4L2_OUTPUT_TYPE_MODULATOR = 1 +V4L2_OUTPUT_TYPE_ANALOG = 2 +V4L2_OUTPUT_TYPE_ANALOGVGAOVERLAY = 3 + +V4L2_OUT_CAP_PRESETS = 0x00000001 +V4L2_OUT_CAP_CUSTOM_TIMINGS = 0x00000002 +V4L2_OUT_CAP_STD = 0x00000004 + +# +# Controls +# + +class v4l2_control(ctypes.Structure): + _fields_ = [ + ('id', ctypes.c_uint32), + ('value', ctypes.c_int32), + ] + + +class v4l2_ext_control(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('value', ctypes.c_int32), + ('value64', ctypes.c_int64), + ('reserved', ctypes.c_void_p), + ] + + _fields_ = [ + ('id', ctypes.c_uint32), + ('reserved2', ctypes.c_uint32 * 2), + ('_u', _u) + ] + + _anonymous_ = ('_u',) + _pack_ = True + + +class v4l2_ext_controls(ctypes.Structure): + _fields_ = [ + ('ctrl_class', ctypes.c_uint32), + ('count', ctypes.c_uint32), + ('error_idx', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 2), + ('controls', ctypes.POINTER(v4l2_ext_control)), + ] + + +V4L2_CTRL_CLASS_USER = 0x00980000 +V4L2_CTRL_CLASS_MPEG = 0x00990000 +V4L2_CTRL_CLASS_CAMERA = 0x009a0000 +V4L2_CTRL_CLASS_FM_TX = 0x009b0000 + + +def V4L2_CTRL_ID_MASK(): + return 0x0fffffff + + +def V4L2_CTRL_ID2CLASS(id_): + return id_ & 0x0fff0000 # unsigned long + + +def V4L2_CTRL_DRIVER_PRIV(id_): + return (id_ & 0xffff) >= 0x1000 + + +class v4l2_queryctrl(ctypes.Structure): + _fields_ = [ + ('id', ctypes.c_uint32), + ('type', v4l2_ctrl_type), + ('name', ctypes.c_char * 32), + ('minimum', ctypes.c_int32), + ('maximum', ctypes.c_int32), + ('step', ctypes.c_int32), + ('default', ctypes.c_int32), + ('flags', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 2), + ] + + +class v4l2_querymenu(ctypes.Structure): + _fields_ = [ + ('id', ctypes.c_uint32), + ('index', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('reserved', ctypes.c_uint32), + ] + + +V4L2_CTRL_FLAG_DISABLED = 0x0001 +V4L2_CTRL_FLAG_GRABBED = 0x0002 +V4L2_CTRL_FLAG_READ_ONLY = 0x0004 +V4L2_CTRL_FLAG_UPDATE = 0x0008 +V4L2_CTRL_FLAG_INACTIVE = 0x0010 +V4L2_CTRL_FLAG_SLIDER = 0x0020 +V4L2_CTRL_FLAG_WRITE_ONLY = 0x0040 + +V4L2_CTRL_FLAG_NEXT_CTRL = 0x80000000 + +V4L2_CID_BASE = V4L2_CTRL_CLASS_USER | 0x900 +V4L2_CID_USER_BASE = V4L2_CID_BASE +V4L2_CID_PRIVATE_BASE = 0x08000000 + +V4L2_CID_USER_CLASS = V4L2_CTRL_CLASS_USER | 1 +V4L2_CID_BRIGHTNESS = V4L2_CID_BASE + 0 +V4L2_CID_CONTRAST = V4L2_CID_BASE + 1 +V4L2_CID_SATURATION = V4L2_CID_BASE + 2 +V4L2_CID_HUE = V4L2_CID_BASE + 3 +V4L2_CID_AUDIO_VOLUME = V4L2_CID_BASE + 5 +V4L2_CID_AUDIO_BALANCE = V4L2_CID_BASE + 6 +V4L2_CID_AUDIO_BASS = V4L2_CID_BASE + 7 +V4L2_CID_AUDIO_TREBLE = V4L2_CID_BASE + 8 +V4L2_CID_AUDIO_MUTE = V4L2_CID_BASE + 9 +V4L2_CID_AUDIO_LOUDNESS = V4L2_CID_BASE + 10 +V4L2_CID_BLACK_LEVEL = V4L2_CID_BASE + 11 # Deprecated +V4L2_CID_AUTO_WHITE_BALANCE = V4L2_CID_BASE + 12 +V4L2_CID_DO_WHITE_BALANCE = V4L2_CID_BASE + 13 +V4L2_CID_RED_BALANCE = V4L2_CID_BASE + 14 +V4L2_CID_BLUE_BALANCE = V4L2_CID_BASE + 15 +V4L2_CID_GAMMA = V4L2_CID_BASE + 16 +V4L2_CID_WHITENESS = V4L2_CID_GAMMA # Deprecated +V4L2_CID_EXPOSURE = V4L2_CID_BASE + 17 +V4L2_CID_AUTOGAIN = V4L2_CID_BASE + 18 +V4L2_CID_GAIN = V4L2_CID_BASE + 19 +V4L2_CID_HFLIP = V4L2_CID_BASE + 20 +V4L2_CID_VFLIP = V4L2_CID_BASE + 21 + +# Deprecated; use V4L2_CID_PAN_RESET and V4L2_CID_TILT_RESET +V4L2_CID_HCENTER = V4L2_CID_BASE + 22 +V4L2_CID_VCENTER = V4L2_CID_BASE + 23 + +V4L2_CID_POWER_LINE_FREQUENCY = V4L2_CID_BASE + 24 + +v4l2_power_line_frequency = enum +( + V4L2_CID_POWER_LINE_FREQUENCY_DISABLED, + V4L2_CID_POWER_LINE_FREQUENCY_50HZ, + V4L2_CID_POWER_LINE_FREQUENCY_60HZ, +) = range(3) + +V4L2_CID_HUE_AUTO = V4L2_CID_BASE + 25 +V4L2_CID_WHITE_BALANCE_TEMPERATURE = V4L2_CID_BASE + 26 +V4L2_CID_SHARPNESS = V4L2_CID_BASE + 27 +V4L2_CID_BACKLIGHT_COMPENSATION = V4L2_CID_BASE + 28 +V4L2_CID_CHROMA_AGC = V4L2_CID_BASE + 29 +V4L2_CID_COLOR_KILLER = V4L2_CID_BASE + 30 +V4L2_CID_COLORFX = V4L2_CID_BASE + 31 + +v4l2_colorfx = enum +( + V4L2_COLORFX_NONE, + V4L2_COLORFX_BW, + V4L2_COLORFX_SEPIA, +) = range(3) + +V4L2_CID_AUTOBRIGHTNESS = V4L2_CID_BASE + 32 +V4L2_CID_BAND_STOP_FILTER = V4L2_CID_BASE + 33 + +V4L2_CID_ROTATE = V4L2_CID_BASE + 34 +V4L2_CID_BG_COLOR = V4L2_CID_BASE + 35 +V4L2_CID_LASTP1 = V4L2_CID_BASE + 36 + +V4L2_CID_MPEG_BASE = V4L2_CTRL_CLASS_MPEG | 0x900 +V4L2_CID_MPEG_CLASS = V4L2_CTRL_CLASS_MPEG | 1 + +# MPEG streams +V4L2_CID_MPEG_STREAM_TYPE = V4L2_CID_MPEG_BASE + 0 + +v4l2_mpeg_stream_type = enum +( + V4L2_MPEG_STREAM_TYPE_MPEG2_PS, + V4L2_MPEG_STREAM_TYPE_MPEG2_TS, + V4L2_MPEG_STREAM_TYPE_MPEG1_SS, + V4L2_MPEG_STREAM_TYPE_MPEG2_DVD, + V4L2_MPEG_STREAM_TYPE_MPEG1_VCD, + V4L2_MPEG_STREAM_TYPE_MPEG2_SVCD, +) = range(6) + +V4L2_CID_MPEG_STREAM_PID_PMT = V4L2_CID_MPEG_BASE + 1 +V4L2_CID_MPEG_STREAM_PID_AUDIO = V4L2_CID_MPEG_BASE + 2 +V4L2_CID_MPEG_STREAM_PID_VIDEO = V4L2_CID_MPEG_BASE + 3 +V4L2_CID_MPEG_STREAM_PID_PCR = V4L2_CID_MPEG_BASE + 4 +V4L2_CID_MPEG_STREAM_PES_ID_AUDIO = V4L2_CID_MPEG_BASE + 5 +V4L2_CID_MPEG_STREAM_PES_ID_VIDEO = V4L2_CID_MPEG_BASE + 6 +V4L2_CID_MPEG_STREAM_VBI_FMT = V4L2_CID_MPEG_BASE + 7 + +v4l2_mpeg_stream_vbi_fmt = enum +( + V4L2_MPEG_STREAM_VBI_FMT_NONE, + V4L2_MPEG_STREAM_VBI_FMT_IVTV, +) = range(2) + +V4L2_CID_MPEG_AUDIO_SAMPLING_FREQ = V4L2_CID_MPEG_BASE + 100 + +v4l2_mpeg_audio_sampling_freq = enum +( + V4L2_MPEG_AUDIO_SAMPLING_FREQ_44100, + V4L2_MPEG_AUDIO_SAMPLING_FREQ_48000, + V4L2_MPEG_AUDIO_SAMPLING_FREQ_32000, +) = range(3) + +V4L2_CID_MPEG_AUDIO_ENCODING = V4L2_CID_MPEG_BASE + 101 + +v4l2_mpeg_audio_encoding = enum +( + V4L2_MPEG_AUDIO_ENCODING_LAYER_1, + V4L2_MPEG_AUDIO_ENCODING_LAYER_2, + V4L2_MPEG_AUDIO_ENCODING_LAYER_3, + V4L2_MPEG_AUDIO_ENCODING_AAC, + V4L2_MPEG_AUDIO_ENCODING_AC3, +) = range(5) + +V4L2_CID_MPEG_AUDIO_L1_BITRATE = V4L2_CID_MPEG_BASE + 102 + +v4l2_mpeg_audio_l1_bitrate = enum +( + V4L2_MPEG_AUDIO_L1_BITRATE_32K, + V4L2_MPEG_AUDIO_L1_BITRATE_64K, + V4L2_MPEG_AUDIO_L1_BITRATE_96K, + V4L2_MPEG_AUDIO_L1_BITRATE_128K, + V4L2_MPEG_AUDIO_L1_BITRATE_160K, + V4L2_MPEG_AUDIO_L1_BITRATE_192K, + V4L2_MPEG_AUDIO_L1_BITRATE_224K, + V4L2_MPEG_AUDIO_L1_BITRATE_256K, + V4L2_MPEG_AUDIO_L1_BITRATE_288K, + V4L2_MPEG_AUDIO_L1_BITRATE_320K, + V4L2_MPEG_AUDIO_L1_BITRATE_352K, + V4L2_MPEG_AUDIO_L1_BITRATE_384K, + V4L2_MPEG_AUDIO_L1_BITRATE_416K, + V4L2_MPEG_AUDIO_L1_BITRATE_448K, +) = range(14) + +V4L2_CID_MPEG_AUDIO_L2_BITRATE = V4L2_CID_MPEG_BASE + 103 + +v4l2_mpeg_audio_l2_bitrate = enum +( + V4L2_MPEG_AUDIO_L2_BITRATE_32K, + V4L2_MPEG_AUDIO_L2_BITRATE_48K, + V4L2_MPEG_AUDIO_L2_BITRATE_56K, + V4L2_MPEG_AUDIO_L2_BITRATE_64K, + V4L2_MPEG_AUDIO_L2_BITRATE_80K, + V4L2_MPEG_AUDIO_L2_BITRATE_96K, + V4L2_MPEG_AUDIO_L2_BITRATE_112K, + V4L2_MPEG_AUDIO_L2_BITRATE_128K, + V4L2_MPEG_AUDIO_L2_BITRATE_160K, + V4L2_MPEG_AUDIO_L2_BITRATE_192K, + V4L2_MPEG_AUDIO_L2_BITRATE_224K, + V4L2_MPEG_AUDIO_L2_BITRATE_256K, + V4L2_MPEG_AUDIO_L2_BITRATE_320K, + V4L2_MPEG_AUDIO_L2_BITRATE_384K, +) = range(14) + +V4L2_CID_MPEG_AUDIO_L3_BITRATE = V4L2_CID_MPEG_BASE + 104 + +v4l2_mpeg_audio_l3_bitrate = enum +( + V4L2_MPEG_AUDIO_L3_BITRATE_32K, + V4L2_MPEG_AUDIO_L3_BITRATE_40K, + V4L2_MPEG_AUDIO_L3_BITRATE_48K, + V4L2_MPEG_AUDIO_L3_BITRATE_56K, + V4L2_MPEG_AUDIO_L3_BITRATE_64K, + V4L2_MPEG_AUDIO_L3_BITRATE_80K, + V4L2_MPEG_AUDIO_L3_BITRATE_96K, + V4L2_MPEG_AUDIO_L3_BITRATE_112K, + V4L2_MPEG_AUDIO_L3_BITRATE_128K, + V4L2_MPEG_AUDIO_L3_BITRATE_160K, + V4L2_MPEG_AUDIO_L3_BITRATE_192K, + V4L2_MPEG_AUDIO_L3_BITRATE_224K, + V4L2_MPEG_AUDIO_L3_BITRATE_256K, + V4L2_MPEG_AUDIO_L3_BITRATE_320K, +) = range(14) + +V4L2_CID_MPEG_AUDIO_MODE = V4L2_CID_MPEG_BASE + 105 + +v4l2_mpeg_audio_mode = enum +( + V4L2_MPEG_AUDIO_MODE_STEREO, + V4L2_MPEG_AUDIO_MODE_JOINT_STEREO, + V4L2_MPEG_AUDIO_MODE_DUAL, + V4L2_MPEG_AUDIO_MODE_MONO, +) = range(4) + +V4L2_CID_MPEG_AUDIO_MODE_EXTENSION = V4L2_CID_MPEG_BASE + 106 + +v4l2_mpeg_audio_mode_extension = enum +( + V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_4, + V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_8, + V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_12, + V4L2_MPEG_AUDIO_MODE_EXTENSION_BOUND_16, +) = range(4) + +V4L2_CID_MPEG_AUDIO_EMPHASIS = V4L2_CID_MPEG_BASE + 107 + +v4l2_mpeg_audio_emphasis = enum +( + V4L2_MPEG_AUDIO_EMPHASIS_NONE, + V4L2_MPEG_AUDIO_EMPHASIS_50_DIV_15_uS, + V4L2_MPEG_AUDIO_EMPHASIS_CCITT_J17, +) = range(3) + +V4L2_CID_MPEG_AUDIO_CRC = V4L2_CID_MPEG_BASE + 108 + +v4l2_mpeg_audio_crc = enum +( + V4L2_MPEG_AUDIO_CRC_NONE, + V4L2_MPEG_AUDIO_CRC_CRC16, +) = range(2) + +V4L2_CID_MPEG_AUDIO_MUTE = V4L2_CID_MPEG_BASE + 109 +V4L2_CID_MPEG_AUDIO_AAC_BITRATE = V4L2_CID_MPEG_BASE + 110 +V4L2_CID_MPEG_AUDIO_AC3_BITRATE = V4L2_CID_MPEG_BASE + 111 + +v4l2_mpeg_audio_ac3_bitrate = enum +( + V4L2_MPEG_AUDIO_AC3_BITRATE_32K, + V4L2_MPEG_AUDIO_AC3_BITRATE_40K, + V4L2_MPEG_AUDIO_AC3_BITRATE_48K, + V4L2_MPEG_AUDIO_AC3_BITRATE_56K, + V4L2_MPEG_AUDIO_AC3_BITRATE_64K, + V4L2_MPEG_AUDIO_AC3_BITRATE_80K, + V4L2_MPEG_AUDIO_AC3_BITRATE_96K, + V4L2_MPEG_AUDIO_AC3_BITRATE_112K, + V4L2_MPEG_AUDIO_AC3_BITRATE_128K, + V4L2_MPEG_AUDIO_AC3_BITRATE_160K, + V4L2_MPEG_AUDIO_AC3_BITRATE_192K, + V4L2_MPEG_AUDIO_AC3_BITRATE_224K, + V4L2_MPEG_AUDIO_AC3_BITRATE_256K, + V4L2_MPEG_AUDIO_AC3_BITRATE_320K, + V4L2_MPEG_AUDIO_AC3_BITRATE_384K, + V4L2_MPEG_AUDIO_AC3_BITRATE_448K, + V4L2_MPEG_AUDIO_AC3_BITRATE_512K, + V4L2_MPEG_AUDIO_AC3_BITRATE_576K, + V4L2_MPEG_AUDIO_AC3_BITRATE_640K, +) = range(19) + +V4L2_CID_MPEG_VIDEO_ENCODING = V4L2_CID_MPEG_BASE + 200 + +v4l2_mpeg_video_encoding = enum +( + V4L2_MPEG_VIDEO_ENCODING_MPEG_1, + V4L2_MPEG_VIDEO_ENCODING_MPEG_2, + V4L2_MPEG_VIDEO_ENCODING_MPEG_4_AVC, +) = range(3) + +V4L2_CID_MPEG_VIDEO_ASPECT = V4L2_CID_MPEG_BASE + 201 + +v4l2_mpeg_video_aspect = enum +( + V4L2_MPEG_VIDEO_ASPECT_1x1, + V4L2_MPEG_VIDEO_ASPECT_4x3, + V4L2_MPEG_VIDEO_ASPECT_16x9, + V4L2_MPEG_VIDEO_ASPECT_221x100, +) = range(4) + +V4L2_CID_MPEG_VIDEO_B_FRAMES = V4L2_CID_MPEG_BASE + 202 +V4L2_CID_MPEG_VIDEO_GOP_SIZE = V4L2_CID_MPEG_BASE + 203 +V4L2_CID_MPEG_VIDEO_GOP_CLOSURE = V4L2_CID_MPEG_BASE + 204 +V4L2_CID_MPEG_VIDEO_PULLDOWN = V4L2_CID_MPEG_BASE + 205 +V4L2_CID_MPEG_VIDEO_BITRATE_MODE = V4L2_CID_MPEG_BASE + 206 + +v4l2_mpeg_video_bitrate_mode = enum +( + V4L2_MPEG_VIDEO_BITRATE_MODE_VBR, + V4L2_MPEG_VIDEO_BITRATE_MODE_CBR, +) = range(2) + +V4L2_CID_MPEG_VIDEO_BITRATE = V4L2_CID_MPEG_BASE + 207 +V4L2_CID_MPEG_VIDEO_BITRATE_PEAK = V4L2_CID_MPEG_BASE + 208 +V4L2_CID_MPEG_VIDEO_TEMPORAL_DECIMATION = V4L2_CID_MPEG_BASE + 209 +V4L2_CID_MPEG_VIDEO_MUTE = V4L2_CID_MPEG_BASE + 210 +V4L2_CID_MPEG_VIDEO_MUTE_YUV = V4L2_CID_MPEG_BASE + 211 + +V4L2_CID_MPEG_CX2341X_BASE = V4L2_CTRL_CLASS_MPEG | 0x1000 +V4L2_CID_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE = V4L2_CID_MPEG_CX2341X_BASE + 0 + +v4l2_mpeg_cx2341x_video_spatial_filter_mode = enum +( + V4L2_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE_MANUAL, + V4L2_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE_AUTO, +) = range(2) + +V4L2_CID_MPEG_CX2341X_VIDEO_SPATIAL_FILTER = V4L2_CID_MPEG_CX2341X_BASE + 1 +V4L2_CID_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE = V4L2_CID_MPEG_CX2341X_BASE + 2 + +v4l2_mpeg_cx2341x_video_luma_spatial_filter_type = enum +( + V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_OFF, + V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_1D_HOR, + V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_1D_VERT, + V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_2D_HV_SEPARABLE, + V4L2_MPEG_CX2341X_VIDEO_LUMA_SPATIAL_FILTER_TYPE_2D_SYM_NON_SEPARABLE, +) = range(5) + +V4L2_CID_MPEG_CX2341X_VIDEO_CHROMA_SPATIAL_FILTER_TYPE = V4L2_CID_MPEG_CX2341X_BASE + 3 + +v4l2_mpeg_cx2341x_video_chroma_spatial_filter_type = enum +( + V4L2_MPEG_CX2341X_VIDEO_CHROMA_SPATIAL_FILTER_TYPE_OFF, + V4L2_MPEG_CX2341X_VIDEO_CHROMA_SPATIAL_FILTER_TYPE_1D_HOR, +) = range(2) + +V4L2_CID_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER_MODE = V4L2_CID_MPEG_CX2341X_BASE + 4 + +v4l2_mpeg_cx2341x_video_temporal_filter_mode = enum +( + V4L2_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER_MODE_MANUAL, + V4L2_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER_MODE_AUTO, +) = range(2) + +V4L2_CID_MPEG_CX2341X_VIDEO_TEMPORAL_FILTER = V4L2_CID_MPEG_CX2341X_BASE + 5 +V4L2_CID_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE = V4L2_CID_MPEG_CX2341X_BASE + 6 + +v4l2_mpeg_cx2341x_video_median_filter_type = enum +( + V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_OFF, + V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_HOR, + V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_VERT, + V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_HOR_VERT, + V4L2_MPEG_CX2341X_VIDEO_MEDIAN_FILTER_TYPE_DIAG, +) = range(5) + +V4L2_CID_MPEG_CX2341X_VIDEO_LUMA_MEDIAN_FILTER_BOTTOM = V4L2_CID_MPEG_CX2341X_BASE + 7 +V4L2_CID_MPEG_CX2341X_VIDEO_LUMA_MEDIAN_FILTER_TOP = V4L2_CID_MPEG_CX2341X_BASE + 8 +V4L2_CID_MPEG_CX2341X_VIDEO_CHROMA_MEDIAN_FILTER_BOTTOM = V4L2_CID_MPEG_CX2341X_BASE + 9 +V4L2_CID_MPEG_CX2341X_VIDEO_CHROMA_MEDIAN_FILTER_TOP = V4L2_CID_MPEG_CX2341X_BASE + 10 +V4L2_CID_MPEG_CX2341X_STREAM_INSERT_NAV_PACKETS = V4L2_CID_MPEG_CX2341X_BASE + 11 + +V4L2_CID_CAMERA_CLASS_BASE = V4L2_CTRL_CLASS_CAMERA | 0x900 +V4L2_CID_CAMERA_CLASS = V4L2_CTRL_CLASS_CAMERA | 1 + +V4L2_CID_EXPOSURE_AUTO = V4L2_CID_CAMERA_CLASS_BASE + 1 + +v4l2_exposure_auto_type = enum +( + V4L2_EXPOSURE_AUTO, + V4L2_EXPOSURE_MANUAL, + V4L2_EXPOSURE_SHUTTER_PRIORITY, + V4L2_EXPOSURE_APERTURE_PRIORITY, +) = range(4) + +V4L2_CID_EXPOSURE_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 2 +V4L2_CID_EXPOSURE_AUTO_PRIORITY = V4L2_CID_CAMERA_CLASS_BASE + 3 + +V4L2_CID_PAN_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 4 +V4L2_CID_TILT_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 5 +V4L2_CID_PAN_RESET = V4L2_CID_CAMERA_CLASS_BASE + 6 +V4L2_CID_TILT_RESET = V4L2_CID_CAMERA_CLASS_BASE + 7 + +V4L2_CID_PAN_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 8 +V4L2_CID_TILT_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 9 + +V4L2_CID_FOCUS_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 10 +V4L2_CID_FOCUS_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 11 +V4L2_CID_FOCUS_AUTO = V4L2_CID_CAMERA_CLASS_BASE + 12 + +V4L2_CID_ZOOM_ABSOLUTE = V4L2_CID_CAMERA_CLASS_BASE + 13 +V4L2_CID_ZOOM_RELATIVE = V4L2_CID_CAMERA_CLASS_BASE + 14 +V4L2_CID_ZOOM_CONTINUOUS = V4L2_CID_CAMERA_CLASS_BASE + 15 + +V4L2_CID_PRIVACY = V4L2_CID_CAMERA_CLASS_BASE + 16 + +V4L2_CID_FM_TX_CLASS_BASE = V4L2_CTRL_CLASS_FM_TX | 0x900 +V4L2_CID_FM_TX_CLASS = V4L2_CTRL_CLASS_FM_TX | 1 + +V4L2_CID_RDS_TX_DEVIATION = V4L2_CID_FM_TX_CLASS_BASE + 1 +V4L2_CID_RDS_TX_PI = V4L2_CID_FM_TX_CLASS_BASE + 2 +V4L2_CID_RDS_TX_PTY = V4L2_CID_FM_TX_CLASS_BASE + 3 +V4L2_CID_RDS_TX_PS_NAME = V4L2_CID_FM_TX_CLASS_BASE + 5 +V4L2_CID_RDS_TX_RADIO_TEXT = V4L2_CID_FM_TX_CLASS_BASE + 6 + +V4L2_CID_AUDIO_LIMITER_ENABLED = V4L2_CID_FM_TX_CLASS_BASE + 64 +V4L2_CID_AUDIO_LIMITER_RELEASE_TIME = V4L2_CID_FM_TX_CLASS_BASE + 65 +V4L2_CID_AUDIO_LIMITER_DEVIATION = V4L2_CID_FM_TX_CLASS_BASE + 66 + +V4L2_CID_AUDIO_COMPRESSION_ENABLED = V4L2_CID_FM_TX_CLASS_BASE + 80 +V4L2_CID_AUDIO_COMPRESSION_GAIN = V4L2_CID_FM_TX_CLASS_BASE + 81 +V4L2_CID_AUDIO_COMPRESSION_THRESHOLD = V4L2_CID_FM_TX_CLASS_BASE + 82 +V4L2_CID_AUDIO_COMPRESSION_ATTACK_TIME = V4L2_CID_FM_TX_CLASS_BASE + 83 +V4L2_CID_AUDIO_COMPRESSION_RELEASE_TIME = V4L2_CID_FM_TX_CLASS_BASE + 84 + +V4L2_CID_PILOT_TONE_ENABLED = V4L2_CID_FM_TX_CLASS_BASE + 96 +V4L2_CID_PILOT_TONE_DEVIATION = V4L2_CID_FM_TX_CLASS_BASE + 97 +V4L2_CID_PILOT_TONE_FREQUENCY = V4L2_CID_FM_TX_CLASS_BASE + 98 + +V4L2_CID_TUNE_PREEMPHASIS = V4L2_CID_FM_TX_CLASS_BASE + 112 + +v4l2_preemphasis = enum +( + V4L2_PREEMPHASIS_DISABLED, + V4L2_PREEMPHASIS_50_uS, + V4L2_PREEMPHASIS_75_uS, +) = range(3) + +V4L2_CID_TUNE_POWER_LEVEL = V4L2_CID_FM_TX_CLASS_BASE + 113 +V4L2_CID_TUNE_ANTENNA_CAPACITOR = V4L2_CID_FM_TX_CLASS_BASE + 114 + + +# +# Tuning +# + +class v4l2_tuner(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('type', v4l2_tuner_type), + ('capability', ctypes.c_uint32), + ('rangelow', ctypes.c_uint32), + ('rangehigh', ctypes.c_uint32), + ('rxsubchans', ctypes.c_uint32), + ('audmode', ctypes.c_uint32), + ('signal', ctypes.c_int32), + ('afc', ctypes.c_int32), + ('reserved', ctypes.c_uint32 * 4), + ] + + +class v4l2_modulator(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('capability', ctypes.c_uint32), + ('rangelow', ctypes.c_uint32), + ('rangehigh', ctypes.c_uint32), + ('txsubchans', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ] + + +V4L2_TUNER_CAP_LOW = 0x0001 +V4L2_TUNER_CAP_NORM = 0x0002 +V4L2_TUNER_CAP_STEREO = 0x0010 +V4L2_TUNER_CAP_LANG2 = 0x0020 +V4L2_TUNER_CAP_SAP = 0x0020 +V4L2_TUNER_CAP_LANG1 = 0x0040 +V4L2_TUNER_CAP_RDS = 0x0080 + +V4L2_TUNER_SUB_MONO = 0x0001 +V4L2_TUNER_SUB_STEREO = 0x0002 +V4L2_TUNER_SUB_LANG2 = 0x0004 +V4L2_TUNER_SUB_SAP = 0x0004 +V4L2_TUNER_SUB_LANG1 = 0x0008 +V4L2_TUNER_SUB_RDS = 0x0010 + +V4L2_TUNER_MODE_MONO = 0x0000 +V4L2_TUNER_MODE_STEREO = 0x0001 +V4L2_TUNER_MODE_LANG2 = 0x0002 +V4L2_TUNER_MODE_SAP = 0x0002 +V4L2_TUNER_MODE_LANG1 = 0x0003 +V4L2_TUNER_MODE_LANG1_LANG2 = 0x0004 + + +class v4l2_frequency(ctypes.Structure): + _fields_ = [ + ('tuner', ctypes.c_uint32), + ('type', v4l2_tuner_type), + ('frequency', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 8), + ] + + +class v4l2_hw_freq_seek(ctypes.Structure): + _fields_ = [ + ('tuner', ctypes.c_uint32), + ('type', v4l2_tuner_type), + ('seek_upward', ctypes.c_uint32), + ('wrap_around', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 8), + ] + + +# +# RDS +# + +class v4l2_rds_data(ctypes.Structure): + _fields_ = [ + ('lsb', ctypes.c_char), + ('msb', ctypes.c_char), + ('block', ctypes.c_char), + ] + + _pack_ = True + + +V4L2_RDS_BLOCK_MSK = 0x7 +V4L2_RDS_BLOCK_A = 0 +V4L2_RDS_BLOCK_B = 1 +V4L2_RDS_BLOCK_C = 2 +V4L2_RDS_BLOCK_D = 3 +V4L2_RDS_BLOCK_C_ALT = 4 +V4L2_RDS_BLOCK_INVALID = 7 + +V4L2_RDS_BLOCK_CORRECTED = 0x40 +V4L2_RDS_BLOCK_ERROR = 0x80 + + +# +# Audio +# + +class v4l2_audio(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('capability', ctypes.c_uint32), + ('mode', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 2), + ] + + +V4L2_AUDCAP_STEREO = 0x00001 +V4L2_AUDCAP_AVL = 0x00002 + +V4L2_AUDMODE_AVL = 0x00001 + + +class v4l2_audioout(ctypes.Structure): + _fields_ = [ + ('index', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ('capability', ctypes.c_uint32), + ('mode', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 2), + ] + + +# +# Mpeg services (experimental) +# + +V4L2_ENC_IDX_FRAME_I = 0 +V4L2_ENC_IDX_FRAME_P = 1 +V4L2_ENC_IDX_FRAME_B = 2 +V4L2_ENC_IDX_FRAME_MASK = 0xf + + +class v4l2_enc_idx_entry(ctypes.Structure): + _fields_ = [ + ('offset', ctypes.c_uint64), + ('pts', ctypes.c_uint64), + ('length', ctypes.c_uint32), + ('flags', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 2), + ] + + +V4L2_ENC_IDX_ENTRIES = 64 + + +class v4l2_enc_idx(ctypes.Structure): + _fields_ = [ + ('entries', ctypes.c_uint32), + ('entries_cap', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 4), + ('entry', v4l2_enc_idx_entry * V4L2_ENC_IDX_ENTRIES), + ] + + +V4L2_ENC_CMD_START = 0 +V4L2_ENC_CMD_STOP = 1 +V4L2_ENC_CMD_PAUSE = 2 +V4L2_ENC_CMD_RESUME = 3 + +V4L2_ENC_CMD_STOP_AT_GOP_END = 1 << 0 + + +class v4l2_encoder_cmd(ctypes.Structure): + class _u(ctypes.Union): + class _s(ctypes.Structure): + _fields_ = [ + ('data', ctypes.c_uint32 * 8), + ] + + _fields_ = [ + ('raw', _s), + ] + + _fields_ = [ + ('cmd', ctypes.c_uint32), + ('flags', ctypes.c_uint32), + ('_u', _u), + ] + + _anonymous_ = ('_u',) + + +# +# Data services (VBI) +# + +class v4l2_vbi_format(ctypes.Structure): + _fields_ = [ + ('sampling_rate', ctypes.c_uint32), + ('offset', ctypes.c_uint32), + ('samples_per_line', ctypes.c_uint32), + ('sample_format', ctypes.c_uint32), + ('start', ctypes.c_int32 * 2), + ('count', ctypes.c_uint32 * 2), + ('flags', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 2), + ] + + +V4L2_VBI_UNSYNC = 1 << 0 +V4L2_VBI_INTERLACED = 1 << 1 + + +class v4l2_sliced_vbi_format(ctypes.Structure): + _fields_ = [ + ('service_set', ctypes.c_uint16), + ('service_lines', ctypes.c_uint16 * 2 * 24), + ('io_size', ctypes.c_uint32), + ('reserved', ctypes.c_uint32 * 2), + ] + + +V4L2_SLICED_TELETEXT_B = 0x0001 +V4L2_SLICED_VPS = 0x0400 +V4L2_SLICED_CAPTION_525 = 0x1000 +V4L2_SLICED_WSS_625 = 0x4000 +V4L2_SLICED_VBI_525 = V4L2_SLICED_CAPTION_525 +V4L2_SLICED_VBI_625 = ( + V4L2_SLICED_TELETEXT_B | V4L2_SLICED_VPS | V4L2_SLICED_WSS_625) + + +class v4l2_sliced_vbi_cap(ctypes.Structure): + _fields_ = [ + ('service_set', ctypes.c_uint16), + ('service_lines', ctypes.c_uint16 * 2 * 24), + ('type', v4l2_buf_type), + ('reserved', ctypes.c_uint32 * 3), + ] + + +class v4l2_sliced_vbi_data(ctypes.Structure): + _fields_ = [ + ('id', ctypes.c_uint32), + ('field', ctypes.c_uint32), + ('line', ctypes.c_uint32), + ('reserved', ctypes.c_uint32), + ('data', ctypes.c_char * 48), + ] + + +# +# Sliced VBI data inserted into MPEG Streams +# + + +V4L2_MPEG_VBI_IVTV_TELETEXT_B = 1 +V4L2_MPEG_VBI_IVTV_CAPTION_525 = 4 +V4L2_MPEG_VBI_IVTV_WSS_625 = 5 +V4L2_MPEG_VBI_IVTV_VPS = 7 + + +class v4l2_mpeg_vbi_itv0_line(ctypes.Structure): + _fields_ = [ + ('id', ctypes.c_char), + ('data', ctypes.c_char * 42), + ] + + _pack_ = True + + +class v4l2_mpeg_vbi_itv0(ctypes.Structure): + _fields_ = [ + ('linemask', ctypes.c_uint32 * 2), # how to define __le32 in ctypes? + ('line', v4l2_mpeg_vbi_itv0_line * 35), + ] + + _pack_ = True + + +class v4l2_mpeg_vbi_ITV0(ctypes.Structure): + _fields_ = [ + ('line', v4l2_mpeg_vbi_itv0_line * 36), + ] + + _pack_ = True + + +V4L2_MPEG_VBI_IVTV_MAGIC0 = "itv0" +V4L2_MPEG_VBI_IVTV_MAGIC1 = "ITV0" + + +class v4l2_mpeg_vbi_fmt_ivtv(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('itv0', v4l2_mpeg_vbi_itv0), + ('ITV0', v4l2_mpeg_vbi_ITV0), + ] + + _fields_ = [ + ('magic', ctypes.c_char * 4), + ('_u', _u) + ] + + _anonymous_ = ('_u',) + _pack_ = True + + +# +# Aggregate structures +# + +class v4l2_format(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('pix', v4l2_pix_format), + ('win', v4l2_window), + ('vbi', v4l2_vbi_format), + ('sliced', v4l2_sliced_vbi_format), + ('raw_data', ctypes.c_char * 200), + ] + + _fields_ = [ + ('type', v4l2_buf_type), + ('fmt', _u), + ] + + +class v4l2_streamparm(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('capture', v4l2_captureparm), + ('output', v4l2_outputparm), + ('raw_data', ctypes.c_char * 200), + ] + + _fields_ = [ + ('type', v4l2_buf_type), + ('parm', _u) + ] + + +# +# Advanced debugging +# + +V4L2_CHIP_MATCH_HOST = 0 +V4L2_CHIP_MATCH_I2C_DRIVER = 1 +V4L2_CHIP_MATCH_I2C_ADDR = 2 +V4L2_CHIP_MATCH_AC97 = 3 + + +class v4l2_dbg_match(ctypes.Structure): + class _u(ctypes.Union): + _fields_ = [ + ('addr', ctypes.c_uint32), + ('name', ctypes.c_char * 32), + ] + + _fields_ = [ + ('type', ctypes.c_uint32), + ('_u', _u), + ] + + _anonymous_ = ('_u',) + _pack_ = True + + +class v4l2_dbg_register(ctypes.Structure): + _fields_ = [ + ('match', v4l2_dbg_match), + ('size', ctypes.c_uint32), + ('reg', ctypes.c_uint64), + ('val', ctypes.c_uint64), + ] + + _pack_ = True + + +class v4l2_dbg_chip_ident(ctypes.Structure): + _fields_ = [ + ('match', v4l2_dbg_match), + ('ident', ctypes.c_uint32), + ('revision', ctypes.c_uint32), + ] + + _pack_ = True + + +# +# ioctl codes for video devices +# + +VIDIOC_QUERYCAP = _IOR('V', 0, v4l2_capability) +VIDIOC_RESERVED = _IO('V', 1) +VIDIOC_ENUM_FMT = _IOWR('V', 2, v4l2_fmtdesc) +VIDIOC_G_FMT = _IOWR('V', 4, v4l2_format) +VIDIOC_S_FMT = _IOWR('V', 5, v4l2_format) +VIDIOC_REQBUFS = _IOWR('V', 8, v4l2_requestbuffers) +VIDIOC_QUERYBUF = _IOWR('V', 9, v4l2_buffer) +VIDIOC_G_FBUF = _IOR('V', 10, v4l2_framebuffer) +VIDIOC_S_FBUF = _IOW('V', 11, v4l2_framebuffer) +VIDIOC_OVERLAY = _IOW('V', 14, ctypes.c_int) +VIDIOC_QBUF = _IOWR('V', 15, v4l2_buffer) +VIDIOC_DQBUF = _IOWR('V', 17, v4l2_buffer) +VIDIOC_STREAMON = _IOW('V', 18, ctypes.c_int) +VIDIOC_STREAMOFF = _IOW('V', 19, ctypes.c_int) +VIDIOC_G_PARM = _IOWR('V', 21, v4l2_streamparm) +VIDIOC_S_PARM = _IOWR('V', 22, v4l2_streamparm) +VIDIOC_G_STD = _IOR('V', 23, v4l2_std_id) +VIDIOC_S_STD = _IOW('V', 24, v4l2_std_id) +VIDIOC_ENUMSTD = _IOWR('V', 25, v4l2_standard) +VIDIOC_ENUMINPUT = _IOWR('V', 26, v4l2_input) +VIDIOC_G_CTRL = _IOWR('V', 27, v4l2_control) +VIDIOC_S_CTRL = _IOWR('V', 28, v4l2_control) +VIDIOC_G_TUNER = _IOWR('V', 29, v4l2_tuner) +VIDIOC_S_TUNER = _IOW('V', 30, v4l2_tuner) +VIDIOC_G_AUDIO = _IOR('V', 33, v4l2_audio) +VIDIOC_S_AUDIO = _IOW('V', 34, v4l2_audio) +VIDIOC_QUERYCTRL = _IOWR('V', 36, v4l2_queryctrl) +VIDIOC_QUERYMENU = _IOWR('V', 37, v4l2_querymenu) +VIDIOC_G_INPUT = _IOR('V', 38, ctypes.c_int) +VIDIOC_S_INPUT = _IOWR('V', 39, ctypes.c_int) +VIDIOC_G_OUTPUT = _IOR('V', 46, ctypes.c_int) +VIDIOC_S_OUTPUT = _IOWR('V', 47, ctypes.c_int) +VIDIOC_ENUMOUTPUT = _IOWR('V', 48, v4l2_output) +VIDIOC_G_AUDOUT = _IOR('V', 49, v4l2_audioout) +VIDIOC_S_AUDOUT = _IOW('V', 50, v4l2_audioout) +VIDIOC_G_MODULATOR = _IOWR('V', 54, v4l2_modulator) +VIDIOC_S_MODULATOR = _IOW('V', 55, v4l2_modulator) +VIDIOC_G_FREQUENCY = _IOWR('V', 56, v4l2_frequency) +VIDIOC_S_FREQUENCY = _IOW('V', 57, v4l2_frequency) +VIDIOC_CROPCAP = _IOWR('V', 58, v4l2_cropcap) +VIDIOC_G_CROP = _IOWR('V', 59, v4l2_crop) +VIDIOC_S_CROP = _IOW('V', 60, v4l2_crop) +VIDIOC_G_JPEGCOMP = _IOR('V', 61, v4l2_jpegcompression) +VIDIOC_S_JPEGCOMP = _IOW('V', 62, v4l2_jpegcompression) +VIDIOC_QUERYSTD = _IOR('V', 63, v4l2_std_id) +VIDIOC_TRY_FMT = _IOWR('V', 64, v4l2_format) +VIDIOC_ENUMAUDIO = _IOWR('V', 65, v4l2_audio) +VIDIOC_ENUMAUDOUT = _IOWR('V', 66, v4l2_audioout) +VIDIOC_G_PRIORITY = _IOR('V', 67, v4l2_priority) +VIDIOC_S_PRIORITY = _IOW('V', 68, v4l2_priority) +VIDIOC_G_SLICED_VBI_CAP = _IOWR('V', 69, v4l2_sliced_vbi_cap) +VIDIOC_LOG_STATUS = _IO('V', 70) +VIDIOC_G_EXT_CTRLS = _IOWR('V', 71, v4l2_ext_controls) +VIDIOC_S_EXT_CTRLS = _IOWR('V', 72, v4l2_ext_controls) +VIDIOC_TRY_EXT_CTRLS = _IOWR('V', 73, v4l2_ext_controls) + +VIDIOC_ENUM_FRAMESIZES = _IOWR('V', 74, v4l2_frmsizeenum) +VIDIOC_ENUM_FRAMEINTERVALS = _IOWR('V', 75, v4l2_frmivalenum) +VIDIOC_G_ENC_INDEX = _IOR('V', 76, v4l2_enc_idx) +VIDIOC_ENCODER_CMD = _IOWR('V', 77, v4l2_encoder_cmd) +VIDIOC_TRY_ENCODER_CMD = _IOWR('V', 78, v4l2_encoder_cmd) + +VIDIOC_DBG_S_REGISTER = _IOW('V', 79, v4l2_dbg_register) +VIDIOC_DBG_G_REGISTER = _IOWR('V', 80, v4l2_dbg_register) + +VIDIOC_DBG_G_CHIP_IDENT = _IOWR('V', 81, v4l2_dbg_chip_ident) + +VIDIOC_S_HW_FREQ_SEEK = _IOW('V', 82, v4l2_hw_freq_seek) +VIDIOC_ENUM_DV_PRESETS = _IOWR('V', 83, v4l2_dv_enum_preset) +VIDIOC_S_DV_PRESET = _IOWR('V', 84, v4l2_dv_preset) +VIDIOC_G_DV_PRESET = _IOWR('V', 85, v4l2_dv_preset) +VIDIOC_QUERY_DV_PRESET = _IOR('V', 86, v4l2_dv_preset) +VIDIOC_S_DV_TIMINGS = _IOWR('V', 87, v4l2_dv_timings) +VIDIOC_G_DV_TIMINGS = _IOWR('V', 88, v4l2_dv_timings) + +VIDIOC_OVERLAY_OLD = _IOWR('V', 14, ctypes.c_int) +VIDIOC_S_PARM_OLD = _IOW('V', 22, v4l2_streamparm) +VIDIOC_S_CTRL_OLD = _IOW('V', 28, v4l2_control) +VIDIOC_G_AUDIO_OLD = _IOWR('V', 33, v4l2_audio) +VIDIOC_G_AUDOUT_OLD = _IOWR('V', 49, v4l2_audioout) +VIDIOC_CROPCAP_OLD = _IOR('V', 58, v4l2_cropcap) + +BASE_VIDIOC_PRIVATE = 192 diff --git a/plugins/light_sensor/icons/extrasoff.svg b/plugins/light_sensor/icons/extrasoff.svg new file mode 100644 index 0000000..a956f03 --- /dev/null +++ b/plugins/light_sensor/icons/extrasoff.svg @@ -0,0 +1,75 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/plugins/light_sensor/icons/extrason.svg b/plugins/light_sensor/icons/extrason.svg new file mode 100644 index 0000000..7ee08bf --- /dev/null +++ b/plugins/light_sensor/icons/extrason.svg @@ -0,0 +1,158 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/light_sensor/icons/sensoroff.svg b/plugins/light_sensor/icons/sensoroff.svg new file mode 100644 index 0000000..0a16670 --- /dev/null +++ b/plugins/light_sensor/icons/sensoroff.svg @@ -0,0 +1,79 @@ + + + +image/svg+xml + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/light_sensor/icons/sensoron.svg b/plugins/light_sensor/icons/sensoron.svg new file mode 100644 index 0000000..d756860 --- /dev/null +++ b/plugins/light_sensor/icons/sensoron.svg @@ -0,0 +1,63 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/plugins/light_sensor/light_sensor.py b/plugins/light_sensor/light_sensor.py new file mode 100644 index 0000000..ebf8587 --- /dev/null +++ b/plugins/light_sensor/light_sensor.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +#Copyright (c) 2011 Walter Bender +# +# 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 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os + +from gettext import gettext as _ + +from plugins.plugin import Plugin + +from TurtleArt.tapalette import make_palette +from TurtleArt.tautils import debug_output +from TurtleArt.taprimitive import Primitive +from TurtleArt.tatype import TYPE_NUMBER + +import logging +_logger = logging.getLogger('turtleart-activity light-sensor plugin') + + +LIGHT_SENSOR_DEVICE = '/sys/devices/platform/olpc-ols.0/level' + + +class Light_sensor(Plugin): + + def __init__(self, parent): + Plugin.__init__(self) + self._parent = parent + if os.path.exists(LIGHT_SENSOR_DEVICE): + self._status = True + else: + self._status = False + self._light = 0 + self.running_sugar = self._parent.running_sugar + + def setup(self): + # set up light-sensor specific blocks + palette = make_palette('extras', + colors=["#FF0000", "#A00000"], + help_string=_('Palette of extra options'), + position=8, + translation=_('extras')) + ''' + palette = make_palette('sensor', + colors=["#FF6060", "#A06060"], + help_string=_('Palette of sensor blocks'), + position=6) + ''' + + if self._status: + palette.add_block('lightsensor', + hidden=True, + style='box-style', + label=_('brightness'), + value_block=True, + help_string=\ + _('light level detected by light sensor'), + prim_name='lightsensor') + else: + palette.add_block('lightsensor', + style='box-style', + label=_('brightness'), + value_block=True, + help_string=\ + _('light level detected by light sensor'), + hidden=True, + prim_name='lightsensor') + + self._parent.lc.def_prim( + 'lightsensor', 0, + Primitive(self.prim_lightsensor, + return_type=TYPE_NUMBER, + call_afterwards=self.after_light)) + + def _status_report(self): + debug_output('Reporting light-sensor status: %s' % (str(self._status))) + return self._status + + # Block primitives + + def prim_lightsensor(self): + if not self._status: + return -1 + else: + fh = open(LIGHT_SENSOR_DEVICE) + string = fh.read() + fh.close() + self._light = float(string) + return self._light + + def after_light(self): + if self._parent.lc.update_values: + self._parent.lc.update_label_value('lightsensor', self._light) diff --git a/plugins/rfid/device.py b/plugins/rfid/device.py new file mode 100644 index 0000000..04a82b2 --- /dev/null +++ b/plugins/rfid/device.py @@ -0,0 +1,61 @@ +import gobject + +class RFIDDevice(gobject.GObject): + """ + Ancestor class for every supported device. + The main class for the device driver must be called "RFIDReader". + """ + # signal "tag-read" has to be emitted when a tag has been read. + # The handler must receive the ISO-11784 hex value of the tag. + # signal "disconnected" has to be emitted when the device is + # unplugged or an error has been detected. + __gsignals__ = { + 'tag-read': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + 'disconnected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_STRING,)) + } + def __init__(self): + """ + Initializer. Subclasses must call this method. + """ + self.__gobject_init__() + + def get_present(self): + """ + This method must detect if the device is present, returning True if so, + or False otherwise. + """ + raise NotImplementedError + + def get_version(self): + """ + Returns a descriptive text of the device. + """ + raise NotImplementedError + + def do_connect(self): + """ + Connects to the device. + Must return True if successfull, False otherwise. + """ + raise NotImplementedError + + def do_disconnect(self): + """ + Disconnects from the device. + """ + raise NotImplementedError + + def read_tag(self): + """ + Returns the 64 bit data in hex format of the last read tag. + """ + raise NotImplementedError + + def write_tag(self, hex_val): + """ + Could be implemented if the device is capable of writing tags. + Receives the hex value (according to ISO 11784) to be written. + Returns True if successfull or False if something went wrong. + """ diff --git a/plugins/rfid/icons/extrasoff.svg b/plugins/rfid/icons/extrasoff.svg new file mode 100644 index 0000000..a956f03 --- /dev/null +++ b/plugins/rfid/icons/extrasoff.svg @@ -0,0 +1,75 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/plugins/rfid/icons/extrason.svg b/plugins/rfid/icons/extrason.svg new file mode 100644 index 0000000..7ee08bf --- /dev/null +++ b/plugins/rfid/icons/extrason.svg @@ -0,0 +1,158 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/rfid/icons/sensoroff.svg b/plugins/rfid/icons/sensoroff.svg new file mode 100644 index 0000000..0a16670 --- /dev/null +++ b/plugins/rfid/icons/sensoroff.svg @@ -0,0 +1,79 @@ + + + +image/svg+xml + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/rfid/icons/sensoron.svg b/plugins/rfid/icons/sensoron.svg new file mode 100644 index 0000000..d756860 --- /dev/null +++ b/plugins/rfid/icons/sensoron.svg @@ -0,0 +1,63 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/plugins/rfid/rfid.py b/plugins/rfid/rfid.py new file mode 100644 index 0000000..ae092e1 --- /dev/null +++ b/plugins/rfid/rfid.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python +#Copyright (C) 2010,11 Emiliano Pastorino +#Copyright (c) 2011 Walter Bender +# +# 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 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import dbus +from dbus.mainloop.glib import DBusGMainLoop +from gettext import gettext as _ + +from plugins.rfid.rfidutils import strhex2bin, strbin2dec, find_device +from plugins.plugin import Plugin + +from TurtleArt.tapalette import make_palette +from TurtleArt.tautils import debug_output +from TurtleArt.taprimitive import Primitive +from TurtleArt.tatype import TYPE_STRING + +import logging +_logger = logging.getLogger('turtleart-activity RFID plugin') + +HAL_SERVICE = 'org.freedesktop.Hal' +HAL_MGR_PATH = '/org/freedesktop/Hal/Manager' +HAL_MGR_IFACE = 'org.freedesktop.Hal.Manager' +HAL_DEV_IFACE = 'org.freedesktop.Hal.Device' +REGEXP_SERUSB = '\/org\/freedesktop\/Hal\/devices\/usb_device['\ + 'a-z,A-Z,0-9,_]*serial_usb_[0-9]' + + +class Rfid(Plugin): + + def __init__(self, parent): + Plugin.__init__(self) + self._parent = parent + self._status = False + + """ + The following code will initialize a USB RFID reader. Please note that + in order to make this initialization function work, it is necessary to + set the permission for the ttyUSB device to 0666. You can do this by + adding a rule to /etc/udev/rules.d + + As root (using sudo or su), copy the following text into a new file in + /etc/udev/rules.d/94-ttyUSB-rules + + KERNEL=="ttyUSB[0-9]",MODE="0666" + + You only have to do this once. + """ + + self.rfid_connected = False + self.rfid_device = find_device(path=parent.path) + self.rfid_idn = '' + + if self.rfid_device is not None: + _logger.info("RFID device found") + self.rfid_connected = self.rfid_device.do_connect() + if self.rfid_connected: + self.rfid_device.connect("tag-read", self._tag_read_cb) + self.rfid_device.connect("disconnected", self._disconnected_cb) + + loop = DBusGMainLoop() + bus = dbus.SystemBus(mainloop=loop) + hmgr_iface = dbus.Interface(bus.get_object(HAL_SERVICE, + HAL_MGR_PATH), HAL_MGR_IFACE) + + hmgr_iface.connect_to_signal('DeviceAdded', self._device_added_cb) + + self._status = True + + def setup(self): + # set up RFID-specific blocks + palette = make_palette('extras', + colors=["#FF0000", "#A00000"], + help_string=_('Palette of extra options'), + position=8, + translation=_('extras')) + ''' + palette = make_palette('sensor', + colors=["#FF6060", "#A06060"], + help_string=_('Palette of sensor blocks'), + position=6) + ''' + + if self._status: + palette.add_block('rfid', + hidden=True, + style='box-style', + label=_('RFID'), + help_string=_('read value from RFID device'), + value_block=True, + prim_name='rfid') + else: + palette.add_block('rfid', + hidden=True, + style='box-style', + label=_('RFID'), + help_string=_('read value from RFID device'), + value_block=True, + prim_name='rfid') + + self._parent.lc.def_prim( + 'rfid', 0, + Primitive(self.prim_read_rfid, + return_type=TYPE_STRING, + call_afterwards=self.after_rfid)) + + def _status_report(self): + debug_output('Reporting RFID status: %s' % (str(self._status))) + return self._status + + def _device_added_cb(self, path): + """ + Called from hal connection when a new device is plugged. + """ + if not self.rfid_connected: + self.rfid_device = find_device(path=parent.path) + _logger.debug("DEVICE_ADDED: %s" % self.rfid_device) + if self.rfid_device is not None: + _logger.debug("DEVICE_ADDED: RFID device is not None!") + self.rfid_connected = self._device.do_connect() + if self.rfid_connected: + _logger.debug("DEVICE_ADDED: Connected!") + self.rfid_device.connect("tag-read", self._tag_read_cb) + self.rfid_device.connect("disconnected", self._disconnected_cb) + + def _disconnected_cb(self, device, text): + """ + Called when the device is disconnected. + """ + self.rfid_connected = False + self.rfid_device = None + + def _tag_read_cb(self, device, tagid): + """ + Callback for "tag-read" signal. Receives the read tag id. + """ + idbin = strhex2bin(tagid) + self.rfid_idn = strbin2dec(idbin[26:64]) + while self.rfid_idn.__len__() < 9: + self.rfid_idn = '0' + self.rfid_idn + print tagid, idbin, self.rfid_idn + self.tw.lc.update_label_value('rfid', self.rfid_idn) + + # Block primitives used in talogo + + def prim_read_rfid(self): + if self._status: + return self.rfid_idn + else: + return '0' + + def after_rfid(self): + if self._parent.lc.update_values: + self._parent.lc.update_label_value('rfid', self.rfid_idn) diff --git a/plugins/rfid/rfidrweusb.py b/plugins/rfid/rfidrweusb.py new file mode 100644 index 0000000..bd12631 --- /dev/null +++ b/plugins/rfid/rfidrweusb.py @@ -0,0 +1,200 @@ +from device import RFIDDevice +from serial import Serial +import dbus +from dbus.mainloop.glib import DBusGMainLoop +import gobject +from time import sleep +import utils + +HAL_SERVICE = 'org.freedesktop.Hal' +HAL_MGR_PATH = '/org/freedesktop/Hal/Manager' +HAL_MGR_IFACE = 'org.freedesktop.Hal.Manager' +HAL_DEV_IFACE = 'org.freedesktop.Hal.Device' +REGEXP_SERUSB = '\/org\/freedesktop\/Hal\/devices\/usb_device['\ + 'a-z,A-Z,0-9,_]*serial_usb_[0-9]' + +VERSIONS = ['301'] + +class RFIDReader(RFIDDevice): + """ + RFIDRW-E-W interface. + """ + + def __init__(self): + + RFIDDevice.__init__(self) + self.last_tag = "" + self.tags = [] + self.ser = Serial() + self.device = '' + self.device_path = '' + self._connected = False + + loop = DBusGMainLoop() + self.bus = dbus.SystemBus(mainloop=loop) + hmgr_iface = dbus.Interface(self.bus.get_object(HAL_SERVICE, + HAL_MGR_PATH), HAL_MGR_IFACE) + + hmgr_iface.connect_to_signal('DeviceRemoved', self._device_removed_cb) + + def get_present(self): + """ + Checks if RFID-RW-USB device is present. + Returns True if so, False otherwise. + """ + hmgr_if = dbus.Interface(self.bus.get_object(HAL_SERVICE, HAL_MGR_PATH), + HAL_MGR_IFACE) + serialusb_devices = set(hmgr_if.FindDeviceStringMatch('serial.type', + 'usb')) & set(hmgr_if.FindDeviceStringMatch( + 'info.subsystem', 'tty')) + for i in serialusb_devices: + serialusb_if = dbus.Interface(self.bus.get_object(HAL_SERVICE, i), + HAL_DEV_IFACE) + if serialusb_if.PropertyExists('info.parent'): + parent_udi = str(serialusb_if.GetProperty('info.parent')) + parent = dbus.Interface(self.bus.get_object(HAL_SERVICE, + parent_udi), HAL_DEV_IFACE) + if parent.PropertyExists('info.linux.driver') and \ + str(parent.GetProperty('info.linux.driver')) == 'ftdi_sio': + device = str(serialusb_if.GetProperty('linux.device_file')) + ser = Serial(device, 9600, timeout=0.1) + ser.read(100) + ser.write('v') + ser.write('e') + ser.write('r') + ser.write('\x0D') + resp = ser.read(4) + if resp[0:-1] in VERSIONS: + self.device = device + self.device_path = i + return True + return False + + def do_connect(self): + """ + Connects to the device. + Returns True if successfull, False otherwise. + """ + retval = False + if self.get_present(): + try: + self.ser = Serial(self.device, 9600, timeout=0.1) + self._connected = True + if self._select_animal_tag: + #gobject.idle_add(self._loop) + gobject.timeout_add(1000, self._loop) + retval = True + except: + self._connected = False + return retval + + def do_disconnect(self): + """ + Disconnect from the device. + """ + self.ser.close() + self._connected = False + + def read_tag(self): + """ + Returns the last read value. + """ + return self.last_tag + + def _select_animal_tag(self): + """ + Sends the "Select Tag 2" (animal tag) command to the device. + """ + self.ser.read(100) + self.ser.write('s') + self.ser.write('t') + self.ser.write('2') + self.ser.write('\x0d') + resp = self.ser.read(3)[0:-1] + if resp == 'OK': + return True + return False + + def get_version(self): + """ + Sends the version command to the device and returns + a string with the device version. + """ + #self.ser.flushInput() + ver = "???" + self.ser.read(100) + self.ser.write('v') + self.ser.write('e') + self.ser.write('r') + self.ser.write('\x0d') + resp = self.ser.read(4)[0:-1] + if resp in VERSIONS: + return "RFIDRW-E-USB " + resp + return ver + + def _device_removed_cb(self, path): + """ + Called when a device is removed. + Checks if the removed device is itself and emits the "disconnected" + signal if so. + """ + if path == self.device_path: + self.device_path = '' + self.ser.close() + self._connected = False + self.tags = [] + self.emit("disconnected","RFID-RW-USB") + + def _loop(self): + """ + Threaded loop for reading data from the device. + """ + if not self._connected: + return False + + self.ser.read(100) + self.ser.write('r') + self.ser.write('a') + self.ser.write('t') + self.ser.write('\x0d') + resp = self.ser.read(33)[0:-1].split('_') + if resp.__len__() is not 6 or resp in self.tags: + return True + + self.tags.append(resp) + anbit_bin = utils.dec2bin(int(resp[2])) + reserved_bin = '00000000000000' + databit_bin = utils.dec2bin(int(resp[3])) + country_bin = utils.dec2bin(int(resp[0])) + while country_bin.__len__() < 10: + country_bin = '0' + country_bin + id_bin = utils.dec2bin(int(resp[1])) + while id_bin.__len__() < 10: + id_bin = '0' + id_bin + + tag_bin = anbit_bin + reserved_bin + databit_bin + country_bin + id_bin + data = utils.bin2hex(tag_bin) + self.emit("tag-read", data) + self.last_tag = data + #sleep(1) + return True + +# Testing +#if __name__ == '__main__': +# def handler(device, idhex): +# """ +# Handler for "tag-read" signal. +# Prints the tag id. +# """ +# print "ID: ", idhex +# +# dev = RFIDReader() +# if dev.get_present(): +# print "SIPI!" +# dev.do_connect() +# dev.connect('tag-read', handler) +# else: +# print "Not connected" +# +# mloop = gobject.MainLoop() +# mloop.run() diff --git a/plugins/rfid/rfidutils.py b/plugins/rfid/rfidutils.py new file mode 100644 index 0000000..a00e518 --- /dev/null +++ b/plugins/rfid/rfidutils.py @@ -0,0 +1,127 @@ +# utils.py - Helper functions for tis2000.py +# Copyright (C) 2010 Emiliano Pastorino +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import logging + +def find_device(path=None): + """ + Search for devices. + Return a device instance or None. + """ + device = None + if path is not None: + path = os.path.join(path, 'plugins/rfid') + else: + path = os.path.join('.', 'plugins/rfid') + for i in os.listdir(path): + if not os.path.isdir(os.path.join(path, i)): + try: + _tempmod = __import__('rfid.%s'%i.split('.')[0], globals(), + locals(), ['RFIDReader'], -1) + devtemp = _tempmod.RFIDReader() + if devtemp.get_present() == True: + device = devtemp + except Exception, e: + # logging.error("FIND_DEVICE: %s: %s"%(i, e)) + pass + if device is None: + logging.debug("No RFID device found") + return device + + +def strhex2bin(strhex): + """ + Convert a string representing an hex value into a + string representing the same value in binary format. + """ + dic = { '0':"0000", + '1':"0001", + '2':"0010", + '3':"0011", + '4':"0100", + '5':"0101", + '6':"0110", + '7':"0111", + '8':"1000", + '9':"1001", + 'A':"1010", + 'B':"1011", + 'C':"1100", + 'D':"1101", + 'E':"1110", + 'F':"1111" + } + binstr = "" + for i in strhex: + binstr = binstr + dic[i.upper()] + return binstr + +def strbin2dec(strbin): + """ + Convert a string representing a binary value into a + string representing the same value in decimal format. + """ + strdec = "0" + for i in range(1, strbin.__len__()+1): + strdec = str(int(strdec)+int(strbin[-i])*int(pow(2, i-1))) + return strdec + +def dec2bin(ndec): + """ + Convert a decimal number into a string representing + the same value in binary format. + """ + if ndec < 1: + return "0" + binary = [] + while ndec != 0: + binary.append(ndec%2) + ndec = ndec/2 + strbin = "" + binary.reverse() + for i in binary: + strbin = strbin+str(i) + return strbin + +def bin2hex(strbin): + """ + Convert a string representing a binary number into a string + representing the same value in hexadecimal format. + """ + dic = { "0000":"0", + "0001":"1", + "0010":"2", + "0011":"3", + "0100":"4", + "0101":"5", + "0110":"6", + "0111":"7", + "1000":"8", + "1001":"9", + "1010":"A", + "1011":"B", + "1100":"C", + "1101":"D", + "1110":"E", + "1111":"F" + } + while strbin.__len__()%4 != 0: + strbin = '0' + strbin + strh = "" + for i in range(0, strbin.__len__()/4): + strh = strh + dic[str(strbin[i*4:i*4+4])] + return strh diff --git a/plugins/rfid/serial/__init__.py b/plugins/rfid/serial/__init__.py new file mode 100644 index 0000000..681ad5c --- /dev/null +++ b/plugins/rfid/serial/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +#portable serial port access with python +#this is a wrapper module for different platform implementations +# +# (C)2001-2002 Chris Liechti +# this is distributed under a free software license, see license.txt + +VERSION = '2.4' + +import sys + +if sys.platform == 'cli': + from serialcli import * +else: + import os + #chose an implementation, depending on os + if os.name == 'nt': #sys.platform == 'win32': + from serialwin32 import * + elif os.name == 'posix': + from serialposix import * + elif os.name == 'java': + from serialjava import * + else: + raise Exception("Sorry: no implementation for your platform ('%s') available" % os.name) + diff --git a/plugins/rfid/serial/serialposix.py b/plugins/rfid/serial/serialposix.py new file mode 100644 index 0000000..174e2f7 --- /dev/null +++ b/plugins/rfid/serial/serialposix.py @@ -0,0 +1,492 @@ +#!/usr/bin/env python +# Python Serial Port Extension for Win32, Linux, BSD, Jython +# module for serial IO for POSIX compatible systems, like Linux +# see __init__.py +# +# (C) 2001-2008 Chris Liechti +# this is distributed under a free software license, see license.txt +# +# parts based on code from Grant B. Edwards : +# ftp://ftp.visi.com/users/grante/python/PosixSerial.py +# references: http://www.easysw.com/~mike/serial/serial.html + +import sys, os, fcntl, termios, struct, select, errno +from serialutil import * + +#Do check the Python version as some constants have moved. +if (sys.hexversion < 0x020100f0): + import TERMIOS +else: + TERMIOS = termios + +if (sys.hexversion < 0x020200f0): + import FCNTL +else: + FCNTL = fcntl + +#try to detect the os so that a device can be selected... +plat = sys.platform.lower() + +if plat[:5] == 'linux': #Linux (confirmed) + def device(port): + return '/dev/ttyS%d' % port + +elif plat == 'cygwin': #cywin/win32 (confirmed) + def device(port): + return '/dev/com%d' % (port + 1) + +elif plat == 'openbsd3': #BSD (confirmed) + def device(port): + return '/dev/ttyp%d' % port + +elif plat[:3] == 'bsd' or \ + plat[:7] == 'freebsd' or \ + plat[:7] == 'openbsd' or \ + plat[:6] == 'darwin': #BSD (confirmed for freebsd4: cuaa%d) + def device(port): + return '/dev/cuad%d' % port + +elif plat[:6] == 'netbsd': #NetBSD 1.6 testing by Erk + def device(port): + return '/dev/dty%02d' % port + +elif plat[:4] == 'irix': #IRIX (partialy tested) + def device(port): + return '/dev/ttyf%d' % (port+1) #XXX different device names depending on flow control + +elif plat[:2] == 'hp': #HP-UX (not tested) + def device(port): + return '/dev/tty%dp0' % (port+1) + +elif plat[:5] == 'sunos': #Solaris/SunOS (confirmed) + def device(port): + return '/dev/tty%c' % (ord('a')+port) + +elif plat[:3] == 'aix': #aix + def device(port): + return '/dev/tty%d' % (port) + +else: + #platform detection has failed... + print """don't know how to number ttys on this system. +! Use an explicit path (eg /dev/ttyS1) or send this information to +! the author of this module: + +sys.platform = %r +os.name = %r +serialposix.py version = %s + +also add the device name of the serial port and where the +counting starts for the first serial port. +e.g. 'first serial port: /dev/ttyS0' +and with a bit luck you can get this module running... +""" % (sys.platform, os.name, VERSION) + #no exception, just continue with a brave attempt to build a device name + #even if the device name is not correct for the platform it has chances + #to work using a string with the real device name as port paramter. + def device(portum): + return '/dev/ttyS%d' % portnum + #~ raise Exception, "this module does not run on this platform, sorry." + +#whats up with "aix", "beos", .... +#they should work, just need to know the device names. + + +#load some constants for later use. +#try to use values from TERMIOS, use defaults from linux otherwise +TIOCMGET = hasattr(TERMIOS, 'TIOCMGET') and TERMIOS.TIOCMGET or 0x5415 +TIOCMBIS = hasattr(TERMIOS, 'TIOCMBIS') and TERMIOS.TIOCMBIS or 0x5416 +TIOCMBIC = hasattr(TERMIOS, 'TIOCMBIC') and TERMIOS.TIOCMBIC or 0x5417 +TIOCMSET = hasattr(TERMIOS, 'TIOCMSET') and TERMIOS.TIOCMSET or 0x5418 + +#TIOCM_LE = hasattr(TERMIOS, 'TIOCM_LE') and TERMIOS.TIOCM_LE or 0x001 +TIOCM_DTR = hasattr(TERMIOS, 'TIOCM_DTR') and TERMIOS.TIOCM_DTR or 0x002 +TIOCM_RTS = hasattr(TERMIOS, 'TIOCM_RTS') and TERMIOS.TIOCM_RTS or 0x004 +#TIOCM_ST = hasattr(TERMIOS, 'TIOCM_ST') and TERMIOS.TIOCM_ST or 0x008 +#TIOCM_SR = hasattr(TERMIOS, 'TIOCM_SR') and TERMIOS.TIOCM_SR or 0x010 + +TIOCM_CTS = hasattr(TERMIOS, 'TIOCM_CTS') and TERMIOS.TIOCM_CTS or 0x020 +TIOCM_CAR = hasattr(TERMIOS, 'TIOCM_CAR') and TERMIOS.TIOCM_CAR or 0x040 +TIOCM_RNG = hasattr(TERMIOS, 'TIOCM_RNG') and TERMIOS.TIOCM_RNG or 0x080 +TIOCM_DSR = hasattr(TERMIOS, 'TIOCM_DSR') and TERMIOS.TIOCM_DSR or 0x100 +TIOCM_CD = hasattr(TERMIOS, 'TIOCM_CD') and TERMIOS.TIOCM_CD or TIOCM_CAR +TIOCM_RI = hasattr(TERMIOS, 'TIOCM_RI') and TERMIOS.TIOCM_RI or TIOCM_RNG +#TIOCM_OUT1 = hasattr(TERMIOS, 'TIOCM_OUT1') and TERMIOS.TIOCM_OUT1 or 0x2000 +#TIOCM_OUT2 = hasattr(TERMIOS, 'TIOCM_OUT2') and TERMIOS.TIOCM_OUT2 or 0x4000 +TIOCINQ = hasattr(TERMIOS, 'FIONREAD') and TERMIOS.FIONREAD or 0x541B + +TIOCM_zero_str = struct.pack('I', 0) +TIOCM_RTS_str = struct.pack('I', TIOCM_RTS) +TIOCM_DTR_str = struct.pack('I', TIOCM_DTR) + +TIOCSBRK = hasattr(TERMIOS, 'TIOCSBRK') and TERMIOS.TIOCSBRK or 0x5427 +TIOCCBRK = hasattr(TERMIOS, 'TIOCCBRK') and TERMIOS.TIOCCBRK or 0x5428 + +ASYNC_SPD_MASK = 0x1030 +ASYNC_SPD_CUST = 0x0030 + +baudrate_constants = { + 0: 0000000, # hang up + 50: 0000001, + 75: 0000002, + 110: 0000003, + 134: 0000004, + 150: 0000005, + 200: 0000006, + 300: 0000007, + 600: 0000010, + 1200: 0000011, + 1800: 0000012, + 2400: 0000013, + 4800: 0000014, + 9600: 0000015, + 19200: 0000016, + 38400: 0000017, + 57600: 0010001, + 115200: 0010002, + 230400: 0010003, + 460800: 0010004, + 500000: 0010005, + 576000: 0010006, + 921600: 0010007, + 1000000: 0010010, + 1152000: 0010011, + 1500000: 0010012, + 2000000: 0010013, + 2500000: 0010014, + 3000000: 0010015, + 3500000: 0010016, + 4000000: 0010017 +} + + +class Serial(SerialBase): + """Serial port class POSIX implementation. Serial port configuration is + done with termios and fcntl. Runs on Linux and many other Un*x like + systems.""" + + def open(self): + """Open port with current settings. This may throw a SerialException + if the port cannot be opened.""" + if self._port is None: + raise SerialException("Port must be configured before it can be used.") + self.fd = None + #open + try: + self.fd = os.open(self.portstr, os.O_RDWR|os.O_NOCTTY|os.O_NONBLOCK) + except Exception, msg: + self.fd = None + raise SerialException("could not open port %s: %s" % (self._port, msg)) + #~ fcntl.fcntl(self.fd, FCNTL.F_SETFL, 0) #set blocking + + try: + self._reconfigurePort() + except: + os.close(self.fd) + self.fd = None + else: + self._isOpen = True + #~ self.flushInput() + + + def _reconfigurePort(self): + """Set communication parameters on opened port.""" + if self.fd is None: + raise SerialException("Can only operate on a valid port handle") + custom_baud = None + + vmin = vtime = 0 #timeout is done via select + if self._interCharTimeout is not None: + vmin = 1 + vtime = int(self._interCharTimeout * 10) + try: + iflag, oflag, cflag, lflag, ispeed, ospeed, cc = termios.tcgetattr(self.fd) + except termios.error, msg: #if a port is nonexistent but has a /dev file, it'll fail here + raise SerialException("Could not configure port: %s" % msg) + #set up raw mode / no echo / binary + cflag |= (TERMIOS.CLOCAL|TERMIOS.CREAD) + lflag &= ~(TERMIOS.ICANON|TERMIOS.ECHO|TERMIOS.ECHOE|TERMIOS.ECHOK|TERMIOS.ECHONL| + TERMIOS.ISIG|TERMIOS.IEXTEN) #|TERMIOS.ECHOPRT + for flag in ('ECHOCTL', 'ECHOKE'): #netbsd workaround for Erk + if hasattr(TERMIOS, flag): + lflag &= ~getattr(TERMIOS, flag) + + oflag &= ~(TERMIOS.OPOST) + iflag &= ~(TERMIOS.INLCR|TERMIOS.IGNCR|TERMIOS.ICRNL|TERMIOS.IGNBRK) + if hasattr(TERMIOS, 'IUCLC'): + iflag &= ~TERMIOS.IUCLC + if hasattr(TERMIOS, 'PARMRK'): + iflag &= ~TERMIOS.PARMRK + + #setup baudrate + try: + ispeed = ospeed = getattr(TERMIOS,'B%s' % (self._baudrate)) + except AttributeError: + try: + ispeed = ospeed = baudrate_constants[self._baudrate] + except KeyError: + #~ raise ValueError('Invalid baud rate: %r' % self._baudrate) + # may need custom baud rate, it isnt in our list. + ispeed = ospeed = getattr(TERMIOS, 'B38400') + custom_baud = int(self._baudrate) # store for later + + #setup char len + cflag &= ~TERMIOS.CSIZE + if self._bytesize == 8: + cflag |= TERMIOS.CS8 + elif self._bytesize == 7: + cflag |= TERMIOS.CS7 + elif self._bytesize == 6: + cflag |= TERMIOS.CS6 + elif self._bytesize == 5: + cflag |= TERMIOS.CS5 + else: + raise ValueError('Invalid char len: %r' % self._bytesize) + #setup stopbits + if self._stopbits == STOPBITS_ONE: + cflag &= ~(TERMIOS.CSTOPB) + elif self._stopbits == STOPBITS_TWO: + cflag |= (TERMIOS.CSTOPB) + else: + raise ValueError('Invalid stopit specification: %r' % self._stopbits) + #setup parity + iflag &= ~(TERMIOS.INPCK|TERMIOS.ISTRIP) + if self._parity == PARITY_NONE: + cflag &= ~(TERMIOS.PARENB|TERMIOS.PARODD) + elif self._parity == PARITY_EVEN: + cflag &= ~(TERMIOS.PARODD) + cflag |= (TERMIOS.PARENB) + elif self._parity == PARITY_ODD: + cflag |= (TERMIOS.PARENB|TERMIOS.PARODD) + else: + raise ValueError('Invalid parity: %r' % self._parity) + #setup flow control + #xonxoff + if hasattr(TERMIOS, 'IXANY'): + if self._xonxoff: + iflag |= (TERMIOS.IXON|TERMIOS.IXOFF) #|TERMIOS.IXANY) + else: + iflag &= ~(TERMIOS.IXON|TERMIOS.IXOFF|TERMIOS.IXANY) + else: + if self._xonxoff: + iflag |= (TERMIOS.IXON|TERMIOS.IXOFF) + else: + iflag &= ~(TERMIOS.IXON|TERMIOS.IXOFF) + #rtscts + if hasattr(TERMIOS, 'CRTSCTS'): + if self._rtscts: + cflag |= (TERMIOS.CRTSCTS) + else: + cflag &= ~(TERMIOS.CRTSCTS) + elif hasattr(TERMIOS, 'CNEW_RTSCTS'): #try it with alternate constant name + if self._rtscts: + cflag |= (TERMIOS.CNEW_RTSCTS) + else: + cflag &= ~(TERMIOS.CNEW_RTSCTS) + #XXX should there be a warning if setting up rtscts (and xonxoff etc) fails?? + + #buffer + #vmin "minimal number of characters to be read. = for non blocking" + if vmin < 0 or vmin > 255: + raise ValueError('Invalid vmin: %r ' % vmin) + cc[TERMIOS.VMIN] = vmin + #vtime + if vtime < 0 or vtime > 255: + raise ValueError('Invalid vtime: %r' % vtime) + cc[TERMIOS.VTIME] = vtime + #activate settings + termios.tcsetattr(self.fd, TERMIOS.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) + + # apply custom baud rate, if any + if custom_baud is not None: + import array + buf = array.array('i', [0] * 32) + + # get serial_struct + FCNTL.ioctl(self.fd, TERMIOS.TIOCGSERIAL, buf) + + # set custom divisor + buf[6] = buf[7] / custom_baud + + # update flags + buf[4] &= ~ASYNC_SPD_MASK + buf[4] |= ASYNC_SPD_CUST + + # set serial_struct + try: + res = FCNTL.ioctl(self.fd, TERMIOS.TIOCSSERIAL, buf) + except IOError: + raise ValueError('Failed to set custom baud rate: %r' % self._baudrate) + + def close(self): + """Close port""" + if self._isOpen: + if self.fd is not None: + os.close(self.fd) + self.fd = None + self._isOpen = False + + def makeDeviceName(self, port): + return device(port) + + # - - - - - - - - - - - - - - - - - - - - - - - - + + def inWaiting(self): + """Return the number of characters currently in the input buffer.""" + #~ s = fcntl.ioctl(self.fd, TERMIOS.FIONREAD, TIOCM_zero_str) + s = fcntl.ioctl(self.fd, TIOCINQ, TIOCM_zero_str) + return struct.unpack('I',s)[0] + + def read(self, size=1): + """Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read.""" + if self.fd is None: raise portNotOpenError + read = '' + inp = None + if size > 0: + while len(read) < size: + #print "\tread(): size",size, "have", len(read) #debug + ready,_,_ = select.select([self.fd],[],[], self._timeout) + if not ready: + break #timeout + buf = os.read(self.fd, size-len(read)) + read = read + buf + if (self._timeout >= 0 or self._interCharTimeout > 0) and not buf: + break #early abort on timeout + return read + + def write(self, data): + """Output the given string over the serial port.""" + if self.fd is None: raise portNotOpenError + if not isinstance(data, str): + raise TypeError('expected str, got %s' % type(data)) + t = len(data) + d = data + while t > 0: + try: + if self._writeTimeout is not None and self._writeTimeout > 0: + _,ready,_ = select.select([],[self.fd],[], self._writeTimeout) + if not ready: + raise writeTimeoutError + n = os.write(self.fd, d) + if self._writeTimeout is not None and self._writeTimeout > 0: + _,ready,_ = select.select([],[self.fd],[], self._writeTimeout) + if not ready: + raise writeTimeoutError + d = d[n:] + t = t - n + except OSError,v: + if v.errno != errno.EAGAIN: + raise + + def flush(self): + """Flush of file like objects. In this case, wait until all data + is written.""" + self.drainOutput() + + def flushInput(self): + """Clear input buffer, discarding all that is in the buffer.""" + if self.fd is None: + raise portNotOpenError + termios.tcflush(self.fd, TERMIOS.TCIFLUSH) + + def flushOutput(self): + """Clear output buffer, aborting the current output and + discarding all that is in the buffer.""" + if self.fd is None: + raise portNotOpenError + termios.tcflush(self.fd, TERMIOS.TCOFLUSH) + + def sendBreak(self, duration=0.25): + """Send break condition. Timed, returns to idle state after given duration.""" + if self.fd is None: + raise portNotOpenError + termios.tcsendbreak(self.fd, int(duration/0.25)) + + def setBreak(self, level=1): + """Set break: Controls TXD. When active, to transmitting is possible.""" + if self.fd is None: raise portNotOpenError + if level: + fcntl.ioctl(self.fd, TIOCSBRK) + else: + fcntl.ioctl(self.fd, TIOCCBRK) + + def setRTS(self, level=1): + """Set terminal status line: Request To Send""" + if self.fd is None: raise portNotOpenError + if level: + fcntl.ioctl(self.fd, TIOCMBIS, TIOCM_RTS_str) + else: + fcntl.ioctl(self.fd, TIOCMBIC, TIOCM_RTS_str) + + def setDTR(self, level=1): + """Set terminal status line: Data Terminal Ready""" + if self.fd is None: raise portNotOpenError + if level: + fcntl.ioctl(self.fd, TIOCMBIS, TIOCM_DTR_str) + else: + fcntl.ioctl(self.fd, TIOCMBIC, TIOCM_DTR_str) + + def getCTS(self): + """Read terminal status line: Clear To Send""" + if self.fd is None: raise portNotOpenError + s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str) + return struct.unpack('I',s)[0] & TIOCM_CTS != 0 + + def getDSR(self): + """Read terminal status line: Data Set Ready""" + if self.fd is None: raise portNotOpenError + s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str) + return struct.unpack('I',s)[0] & TIOCM_DSR != 0 + + def getRI(self): + """Read terminal status line: Ring Indicator""" + if self.fd is None: raise portNotOpenError + s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str) + return struct.unpack('I',s)[0] & TIOCM_RI != 0 + + def getCD(self): + """Read terminal status line: Carrier Detect""" + if self.fd is None: raise portNotOpenError + s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str) + return struct.unpack('I',s)[0] & TIOCM_CD != 0 + + # - - platform specific - - - - + + def drainOutput(self): + """internal - not portable!""" + if self.fd is None: raise portNotOpenError + termios.tcdrain(self.fd) + + def nonblocking(self): + """internal - not portable!""" + if self.fd is None: + raise portNotOpenError + fcntl.fcntl(self.fd, FCNTL.F_SETFL, FCNTL.O_NONBLOCK) + + def fileno(self): + """For easier of the serial port instance with select. + WARNING: this function is not portable to different platforms!""" + if self.fd is None: raise portNotOpenError + return self.fd + +if __name__ == '__main__': + s = Serial(0, + baudrate=19200, #baudrate + bytesize=EIGHTBITS, #number of databits + parity=PARITY_EVEN, #enable parity checking + stopbits=STOPBITS_ONE, #number of stopbits + timeout=3, #set a timeout value, None for waiting forever + xonxoff=0, #enable software flow control + rtscts=0, #enable RTS/CTS flow control + ) + s.setRTS(1) + s.setDTR(1) + s.flushInput() + s.flushOutput() + s.write('hello') + print repr(s.read(5)) + print s.inWaiting() + del s + diff --git a/plugins/rfid/serial/serialutil.py b/plugins/rfid/serial/serialutil.py new file mode 100644 index 0000000..fd466f2 --- /dev/null +++ b/plugins/rfid/serial/serialutil.py @@ -0,0 +1,400 @@ +#! python +# Python Serial Port Extension for Win32, Linux, BSD, Jython +# see __init__.py +# +# (C) 2001-2008 Chris Liechti +# this is distributed under a free software license, see license.txt + +PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE = 'N', 'E', 'O', 'M', 'S' +STOPBITS_ONE, STOPBITS_TWO = (1, 2) +FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS = (5,6,7,8) + +PARITY_NAMES = { + PARITY_NONE: 'None', + PARITY_EVEN: 'Even', + PARITY_ODD: 'Odd', + PARITY_MARK: 'Mark', + PARITY_SPACE:'Space', +} + +XON = chr(17) +XOFF = chr(19) + +#Python < 2.2.3 compatibility +try: + True +except: + True = 1 + False = not True + +class SerialException(Exception): + """Base class for serial port related exceptions.""" + +portNotOpenError = SerialException('Port not open') + +class SerialTimeoutException(SerialException): + """Write timeouts give an exception""" + +writeTimeoutError = SerialTimeoutException("Write timeout") + +class FileLike(object): + """An abstract file like class. + + This class implements readline and readlines based on read and + writelines based on write. + This class is used to provide the above functions for to Serial + port objects. + + Note that when the serial port was opened with _NO_ timeout that + readline blocks until it sees a newline (or the specified size is + reached) and that readlines would never return and therefore + refuses to work (it raises an exception in this case)! + """ + + def read(self, size): raise NotImplementedError + def write(self, s): raise NotImplementedError + + def readline(self, size=None, eol='\n'): + """read a line which is terminated with end-of-line (eol) character + ('\n' by default) or until timeout""" + line = '' + while 1: + c = self.read(1) + if c: + line += c #not very efficient but lines are usually not that long + if c == eol: + break + if size is not None and len(line) >= size: + break + else: + break + return line + + def readlines(self, sizehint=None, eol='\n'): + """read a list of lines, until timeout + sizehint is ignored""" + if self.timeout is None: + raise ValueError, "Serial port MUST have enabled timeout for this function!" + lines = [] + while 1: + line = self.readline(eol=eol) + if line: + lines.append(line) + if line[-1] != eol: #was the line received with a timeout? + break + else: + break + return lines + + def xreadlines(self, sizehint=None): + """just call readlines - here for compatibility""" + return self.readlines() + + def writelines(self, sequence): + for line in sequence: + self.write(line) + + def flush(self): + """flush of file like objects""" + pass + + # iterator for e.g. "for line in Serial(0): ..." usage + def next(self): + line = self.readline() + if not line: raise StopIteration + return line + + def __iter__(self): + return self + + +class SerialBase(FileLike): + """Serial port base class. Provides __init__ function and properties to + get/set port settings.""" + + #default values, may be overriden in subclasses that do not support all values + BAUDRATES = (50,75,110,134,150,200,300,600,1200,1800,2400,4800,9600, + 19200,38400,57600,115200,230400,460800,500000,576000,921600, + 1000000,1152000,1500000,2000000,2500000,3000000,3500000,4000000) + BYTESIZES = (FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS) + PARITIES = (PARITY_NONE, PARITY_EVEN, PARITY_ODD) + STOPBITS = (STOPBITS_ONE, STOPBITS_TWO) + + def __init__(self, + port = None, #number of device, numbering starts at + #zero. if everything fails, the user + #can specify a device string, note + #that this isn't portable anymore + #port will be opened if one is specified + baudrate=9600, #baudrate + bytesize=EIGHTBITS, #number of databits + parity=PARITY_NONE, #enable parity checking + stopbits=STOPBITS_ONE, #number of stopbits + timeout=None, #set a timeout value, None to wait forever + xonxoff=0, #enable software flow control + rtscts=0, #enable RTS/CTS flow control + writeTimeout=None, #set a timeout for writes + dsrdtr=None, #None: use rtscts setting, dsrdtr override if true or false + interCharTimeout=None #Inter-character timeout, None to disable + ): + """Initialize comm port object. If a port is given, then the port will be + opened immediately. Otherwise a Serial port object in closed state + is returned.""" + + self._isOpen = False + self._port = None #correct value is assigned below trough properties + self._baudrate = None #correct value is assigned below trough properties + self._bytesize = None #correct value is assigned below trough properties + self._parity = None #correct value is assigned below trough properties + self._stopbits = None #correct value is assigned below trough properties + self._timeout = None #correct value is assigned below trough properties + self._writeTimeout = None #correct value is assigned below trough properties + self._xonxoff = None #correct value is assigned below trough properties + self._rtscts = None #correct value is assigned below trough properties + self._dsrdtr = None #correct value is assigned below trough properties + self._interCharTimeout = None #correct value is assigned below trough properties + + #assign values using get/set methods using the properties feature + self.port = port + self.baudrate = baudrate + self.bytesize = bytesize + self.parity = parity + self.stopbits = stopbits + self.timeout = timeout + self.writeTimeout = writeTimeout + self.xonxoff = xonxoff + self.rtscts = rtscts + self.dsrdtr = dsrdtr + self.interCharTimeout = interCharTimeout + + if port is not None: + self.open() + + def isOpen(self): + """Check if the port is opened.""" + return self._isOpen + + # - - - - - - - - - - - - - - - - - - - - - - - - + + #TODO: these are not realy needed as the is the BAUDRATES etc attribute... + #maybe i remove them before the final release... + + def getSupportedBaudrates(self): + return [(str(b), b) for b in self.BAUDRATES] + + def getSupportedByteSizes(self): + return [(str(b), b) for b in self.BYTESIZES] + + def getSupportedStopbits(self): + return [(str(b), b) for b in self.STOPBITS] + + def getSupportedParities(self): + return [(PARITY_NAMES[b], b) for b in self.PARITIES] + + # - - - - - - - - - - - - - - - - - - - - - - - - + + def setPort(self, port): + """Change the port. The attribute portstr is set to a string that + contains the name of the port.""" + + was_open = self._isOpen + if was_open: self.close() + if port is not None: + if type(port) in [type(''), type(u'')]: #strings are taken directly + self.portstr = port + else: + self.portstr = self.makeDeviceName(port) + else: + self.portstr = None + self._port = port + if was_open: self.open() + + def getPort(self): + """Get the current port setting. The value that was passed on init or using + setPort() is passed back. See also the attribute portstr which contains + the name of the port as a string.""" + return self._port + + port = property(getPort, setPort, doc="Port setting") + + + def setBaudrate(self, baudrate): + """Change baudrate. It raises a ValueError if the port is open and the + baudrate is not possible. If the port is closed, then tha value is + accepted and the exception is raised when the port is opened.""" + #~ if baudrate not in self.BAUDRATES: raise ValueError("Not a valid baudrate: %r" % baudrate) + try: + self._baudrate = int(baudrate) + except TypeError: + raise ValueError("Not a valid baudrate: %r" % (baudrate,)) + else: + if self._isOpen: self._reconfigurePort() + + def getBaudrate(self): + """Get the current baudrate setting.""" + return self._baudrate + + baudrate = property(getBaudrate, setBaudrate, doc="Baudrate setting") + + + def setByteSize(self, bytesize): + """Change byte size.""" + if bytesize not in self.BYTESIZES: raise ValueError("Not a valid byte size: %r" % (bytesize,)) + self._bytesize = bytesize + if self._isOpen: self._reconfigurePort() + + def getByteSize(self): + """Get the current byte size setting.""" + return self._bytesize + + bytesize = property(getByteSize, setByteSize, doc="Byte size setting") + + + def setParity(self, parity): + """Change parity setting.""" + if parity not in self.PARITIES: raise ValueError("Not a valid parity: %r" % (parity,)) + self._parity = parity + if self._isOpen: self._reconfigurePort() + + def getParity(self): + """Get the current parity setting.""" + return self._parity + + parity = property(getParity, setParity, doc="Parity setting") + + + def setStopbits(self, stopbits): + """Change stopbits size.""" + if stopbits not in self.STOPBITS: raise ValueError("Not a valid stopbit size: %r" % (stopbits,)) + self._stopbits = stopbits + if self._isOpen: self._reconfigurePort() + + def getStopbits(self): + """Get the current stopbits setting.""" + return self._stopbits + + stopbits = property(getStopbits, setStopbits, doc="Stopbits setting") + + + def setTimeout(self, timeout): + """Change timeout setting.""" + if timeout is not None: + if timeout < 0: raise ValueError("Not a valid timeout: %r" % (timeout,)) + try: + timeout + 1 #test if it's a number, will throw a TypeError if not... + except TypeError: + raise ValueError("Not a valid timeout: %r" % (timeout,)) + + self._timeout = timeout + if self._isOpen: self._reconfigurePort() + + def getTimeout(self): + """Get the current timeout setting.""" + return self._timeout + + timeout = property(getTimeout, setTimeout, doc="Timeout setting for read()") + + + def setWriteTimeout(self, timeout): + """Change timeout setting.""" + if timeout is not None: + if timeout < 0: raise ValueError("Not a valid timeout: %r" % (timeout,)) + try: + timeout + 1 #test if it's a number, will throw a TypeError if not... + except TypeError: + raise ValueError("Not a valid timeout: %r" % timeout) + + self._writeTimeout = timeout + if self._isOpen: self._reconfigurePort() + + def getWriteTimeout(self): + """Get the current timeout setting.""" + return self._writeTimeout + + writeTimeout = property(getWriteTimeout, setWriteTimeout, doc="Timeout setting for write()") + + + def setXonXoff(self, xonxoff): + """Change XonXoff setting.""" + self._xonxoff = xonxoff + if self._isOpen: self._reconfigurePort() + + def getXonXoff(self): + """Get the current XonXoff setting.""" + return self._xonxoff + + xonxoff = property(getXonXoff, setXonXoff, doc="Xon/Xoff setting") + + def setRtsCts(self, rtscts): + """Change RtsCts flow control setting.""" + self._rtscts = rtscts + if self._isOpen: self._reconfigurePort() + + def getRtsCts(self): + """Get the current RtsCts flow control setting.""" + return self._rtscts + + rtscts = property(getRtsCts, setRtsCts, doc="RTS/CTS flow control setting") + + def setDsrDtr(self, dsrdtr=None): + """Change DsrDtr flow control setting.""" + if dsrdtr is None: + #if not set, keep backwards compatibility and follow rtscts setting + self._dsrdtr = self._rtscts + else: + #if defined independently, follow its value + self._dsrdtr = dsrdtr + if self._isOpen: self._reconfigurePort() + + def getDsrDtr(self): + """Get the current DsrDtr flow control setting.""" + return self._dsrdtr + + dsrdtr = property(getDsrDtr, setDsrDtr, "DSR/DTR flow control setting") + + def setInterCharTimeout(self, interCharTimeout): + """Change inter-character timeout setting.""" + if interCharTimeout is not None: + if interCharTimeout < 0: raise ValueError("Not a valid timeout: %r" % interCharTimeout) + try: + interCharTimeout + 1 #test if it's a number, will throw a TypeError if not... + except TypeError: + raise ValueError("Not a valid timeout: %r" % interCharTimeout) + + self._interCharTimeout = interCharTimeout + if self._isOpen: self._reconfigurePort() + + def getInterCharTimeout(self): + """Get the current inter-character timeout setting.""" + return self._interCharTimeout + + interCharTimeout = property(getInterCharTimeout, setInterCharTimeout, doc="Inter-character timeout setting for read()") + + + # - - - - - - - - - - - - - - - - - - - - - - - - + + def __repr__(self): + """String representation of the current port settings and its state.""" + return "%s(port=%r, baudrate=%r, bytesize=%r, parity=%r, stopbits=%r, timeout=%r, xonxoff=%r, rtscts=%r, dsrdtr=%r)" % ( + self.__class__.__name__, + id(self), + self._isOpen, + self.portstr, + self.baudrate, + self.bytesize, + self.parity, + self.stopbits, + self.timeout, + self.xonxoff, + self.rtscts, + self.dsrdtr, + ) + +if __name__ == '__main__': + s = SerialBase() + print s.portstr + print s.getSupportedBaudrates() + print s.getSupportedByteSizes() + print s.getSupportedParities() + print s.getSupportedStopbits() + print s diff --git a/plugins/rfid/tis2000.py b/plugins/rfid/tis2000.py new file mode 100644 index 0000000..91d1991 --- /dev/null +++ b/plugins/rfid/tis2000.py @@ -0,0 +1,252 @@ +from device import RFIDDevice +from serial import Serial +import dbus +from dbus.mainloop.glib import DBusGMainLoop +import gobject +import re +from time import sleep + +HAL_SERVICE = 'org.freedesktop.Hal' +HAL_MGR_PATH = '/org/freedesktop/Hal/Manager' +HAL_MGR_IFACE = 'org.freedesktop.Hal.Manager' +HAL_DEV_IFACE = 'org.freedesktop.Hal.Device' +REGEXP_SERUSB = '\/org\/freedesktop\/Hal\/devices\/usb_device['\ + 'a-z,A-Z,0-9,_]*serial_usb_[0-9]' + +STATE_WAITING = 0 +STATE_WAITING2 = 1 +STATE_READING = 2 + +class RFIDReader(RFIDDevice): + """ + TIS-2000 interface. + """ + + def __init__(self): + + RFIDDevice.__init__(self) + self.last_tag = "" + self.ser = Serial() + self.device = '' + self.device_path = '' + self._connected = False + self._state = STATE_WAITING + + loop = DBusGMainLoop() + self.bus = dbus.SystemBus(mainloop=loop) + hmgr_iface = dbus.Interface(self.bus.get_object(HAL_SERVICE, + HAL_MGR_PATH), HAL_MGR_IFACE) + + hmgr_iface.connect_to_signal('DeviceRemoved', self._device_removed_cb) + + def get_present(self): + """ + Checks if TI-S2000 device is present. + Returns True if so, False otherwise. + """ + hmgr_if = dbus.Interface(self.bus.get_object(HAL_SERVICE, HAL_MGR_PATH), + HAL_MGR_IFACE) + tiusb_devices = set(hmgr_if.FindDeviceStringMatch('serial.type', + 'usb')) & set(hmgr_if.FindDeviceStringMatch( + 'info.product', 'TUSB3410 Microcontroller')) + for i in tiusb_devices: + tiusb_if = dbus.Interface(self.bus.get_object(HAL_SERVICE, i), + HAL_DEV_IFACE) + if tiusb_if.PropertyExists('linux.device_file'): + self.device = str(tiusb_if.GetProperty('linux.device_file')) + self.device_path = i + return True + return False + + def do_connect(self): + """ + Connects to the device. + Returns True if successfull, False otherwise. + """ + retval = False + if self.get_present(): + try: + self.ser = Serial(self.device, 9600, timeout=0.1) + self._connected = True + self._escape() + self._clear() + self._format() + gobject.idle_add(self._loop) + retval = True + except: + self._connected = False + return retval + + def do_disconnect(self): + """ + Disconnect from the device. + """ + self.ser.close() + self._connected = False + + def read_tag(self): + """ + Returns the last read value. + """ + return self.last_tag + + def write_tag(self, hexval): + """ + Usage: write_tag(hexval) + + Writes the hexadecimal string "hexval" into the tag. + Returns True if successfull, False otherwise. + """ + #self.ser.flushInput() + reg = re.compile('([^0-9A-F]+)') + if not (hexval.__len__() == 16 and reg.findall(hexval) == []): + return False + self.ser.read(100) + self.ser.write('P') + for i in hexval: + self.ser.write(i) + sleep(1) + resp = self.ser.read(64) + resp = resp.split()[0] + if resp == "P0": + return True + else: + return False + + def _escape(self): + """ + Sends the scape command to the TIS-2000 device. + """ + try: + #self.ser.flushInput() + self.ser.read(100) + self.ser.write('\x1B') + resp = self.ser.read() + if resp == 'E': + return True + else: + return False + except: + return False + + def _format(self): + """ + Sends the format command to the TIS-2000 device. + """ + try: + #self.ser.flushInput() + self.ser.read(100) + self.ser.write('F') + resp = self.ser.read() + if resp == 'F': + return True + else: + return False + except: + return False + + def _clear(self): + """ + Sends the clear command to the TIS-2000 device. + """ + try: + #self.ser.flushInput() + self.ser.read(100) + self.ser.write('C') + resp = self.ser.read() + if resp == 'C': + return True + else: + return False + except: + return False + + def get_version(self): + """ + Sends the version command to the TIS-2000 device and returns + a string with the device version. + """ + #self.ser.flushInput() + self.ser.read(100) + self.ser.write('V') + version = [] + tver = "" + while 1: + resp = self.ser.read() + if resp == '\x0A' or resp == '': + break + if resp != '\n' and resp != '\r': + version.append(resp) + for i in version: + tver = tver + i + if tver != "": + return tver + return "Unknown" + + def _device_removed_cb(self, path): + """ + Called when a device is removed. + Checks if the removed device is itself and emits the "disconnected" + signal if so. + """ + if path == self.device_path: + self.device_path = '' + self.ser.close() + self._connected = False + self.emit("disconnected","TIS-2000") + + def _loop(self): + """ + Threaded loop for reading data sent from the TIS-2000. + """ + if not self._connected: + return False + + if self._state is STATE_WAITING: + data = self.ser.read() + if data in ['W', 'R']: + self._state = STATE_WAITING2 + return True + + elif self._state is STATE_WAITING2: + data = self.ser.read() + if data.isspace(): + self._state = STATE_READING + else: + self._clear() + self._state = STATE_WAITING + return True + + elif self._state is STATE_READING: + data = self.ser.read(16) + if data.__len__() < 16: + self._clear() + self._state = STATE_WAITING + else: + reg = re.compile('([^0-9A-F]+)') + if reg.findall(data) == []: + self.emit("tag-read", data) + self.last_tag = data + self._clear() + self._state = STATE_WAITING + return True + return True + +# Testing +#if __name__ == '__main__': +# def handler(device, idhex): +# """ +# Handler for "tag-read" signal. +# Prints the tag id. +# """ +# print "ID: ", idhex +# +# dev = RFIDReader() +# if dev.get_present(): +# dev.do_connect() +# dev.connect('tag-read', handler) +# else: +# print "Not connected" +# +# mloop = gobject.MainLoop() +# mloop.run() diff --git a/plugins/rfid/utils.py b/plugins/rfid/utils.py new file mode 100644 index 0000000..94e5540 --- /dev/null +++ b/plugins/rfid/utils.py @@ -0,0 +1,98 @@ +# utils.py - Helper functions for tis2000.py +# Copyright (C) 2010 Emiliano Pastorino +# +# 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +def strhex2bin(strhex): + """ + Convert a string representing an hex value into a + string representing the same value in binary format. + """ + dic = { '0':"0000", + '1':"0001", + '2':"0010", + '3':"0011", + '4':"0100", + '5':"0101", + '6':"0110", + '7':"0111", + '8':"1000", + '9':"1001", + 'A':"1010", + 'B':"1011", + 'C':"1100", + 'D':"1101", + 'E':"1110", + 'F':"1111" + } + binstr = "" + for i in strhex: + binstr = binstr + dic[i.upper()] + return binstr + +def strbin2dec(strbin): + """ + Convert a string representing a binary value into a + string representing the same value in decimal format. + """ + strdec = "0" + for i in range(1, strbin.__len__()+1): + strdec = str(int(strdec)+int(strbin[-i])*int(pow(2, i-1))) + return strdec + +def dec2bin(ndec): + """ + Convert a decimal number into a string representing + the same value in binary format. + """ + if ndec < 1: + return "0" + binary = [] + while ndec != 0: + binary.append(ndec%2) + ndec = ndec/2 + strbin = "" + binary.reverse() + for i in binary: + strbin = strbin+str(i) + return strbin + +def bin2hex(strbin): + """ + Convert a string representing a binary number into a string + representing the same value in hexadecimal format. + """ + dic = { "0000":"0", + "0001":"1", + "0010":"2", + "0011":"3", + "0100":"4", + "0101":"5", + "0110":"6", + "0111":"7", + "1000":"8", + "1001":"9", + "1010":"A", + "1011":"B", + "1100":"C", + "1101":"D", + "1110":"E", + "1111":"F" + } + while strbin.__len__()%4 != 0: + strbin = '0' + strbin + strh = "" + for i in range(0, strbin.__len__()/4): + strh = strh + dic[str(strbin[i*4:i*4+4])] + return strh diff --git a/plugins/turtle_blocks_extras/turtle_blocks_extras.py b/plugins/turtle_blocks_extras/turtle_blocks_extras.py index ddeb63d..66f3b70 100644 --- a/plugins/turtle_blocks_extras/turtle_blocks_extras.py +++ b/plugins/turtle_blocks_extras/turtle_blocks_extras.py @@ -15,8 +15,6 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -import gtk -import gobject from time import time import os import glob @@ -24,36 +22,19 @@ import glob from gettext import gettext as _ from plugins.plugin import Plugin -from TurtleArt.tapalette import (make_palette, define_logo_function, - block_names, block_primitives, special_names, - content_blocks, palette_name_to_index, - palette_names, palette_i18n_names) -from TurtleArt.talogo import (primitive_dictionary, logoerror, - media_blocks_dictionary) -from TurtleArt.taconstants import (DEFAULT_SCALE, ICON_SIZE, CONSTANTS, - MEDIA_SHAPES, SKIN_PATHS, BLOCKS_WITH_SKIN, - PYTHON_SKIN, PREFIX_DICTIONARY, VOICES, - MACROS, COLORDICT) -from TurtleArt.tautils import (round_int, debug_output, get_path, - data_to_string, find_group, image_to_base64, +from TurtleArt.tapalette import (make_palette, define_logo_function) +from TurtleArt.talogo import (primitive_dictionary, logoerror) +from TurtleArt.taconstants import (CONSTANTS, MACROS, KEY_DICT, MEDIA_SHAPES, + REVERSE_KEY_DICT, SKIN_PATHS, + BLOCKS_WITH_SKIN, PYTHON_SKIN, + MEDIA_BLOCK2TYPE, VOICES) +from TurtleArt.tautils import (debug_output, get_path, data_to_string, hat_on_top, listify, data_from_file) -from TurtleArt.tajail import (myfunc, myfunc_import) - - -def _num_type(x): - """ Is x a number type? """ - if type(x) == int: - return True - if type(x) == float: - return True - if type(x) == ord: - return True - return False - - -def _millisecond(): - """ Current time in milliseconds """ - return time() * 1000 +from TurtleArt.taprimitive import (ArgSlot, ConstantArg, Primitive) +from TurtleArt.tatype import (TYPE_BOOL, TYPE_BOX, TYPE_CHAR, TYPE_INT, + TYPE_FLOAT, TYPE_OBJECT, TYPE_STRING, + TYPE_NUMBER) +from TurtleArt.taturtle import Turtle class Turtle_blocks_extras(Plugin): @@ -61,13 +42,13 @@ class Turtle_blocks_extras(Plugin): from Turtle Art """ def __init__(self, turtle_window): + Plugin.__init__(self) self.tw = turtle_window def setup(self): SKIN_PATHS.append('plugins/turtle_blocks_extras/images') self.heap = self.tw.lc.heap - self.keyboard = self.tw.lc.keyboard self.title_height = int((self.tw.canvas.height / 20) * self.tw.scale) # set up Turtle Block palettes @@ -90,45 +71,68 @@ class Turtle_blocks_extras(Plugin): colors=["#FFC000", "#A08000"], help_string=_('Palette of flow operators')) - # internally expanded macro palette.add_block('while', - hidden=True, style='clamp-style-boolean', label=_('while'), prim_name='while', - default=[None, None, None], + default=[None, None], special_name=_('while'), help_string=_('do-while-True operator that uses \ boolean operators from Numbers palette')) + self.tw.lc.def_prim( + 'while', 2, + Primitive(self.tw.lc.prim_loop, + arg_descs=[ + ArgSlot(TYPE_OBJECT, + call_arg=False, + wrapper=Primitive( + Primitive.controller_while, + arg_descs=[ArgSlot(TYPE_BOOL, + call_arg=False)])), + ArgSlot(TYPE_OBJECT)]), + True) - # internally expanded macro palette.add_block('until', - hidden=True, - style='clamp-style-boolean', + style='clamp-style-until', label=_('until'), prim_name='until', - default=[None, None, None], + default=[None, None], special_name=_('until'), help_string=_('do-until-True operator that uses \ boolean operators from Numbers palette')) + self.tw.lc.def_prim( + 'until', 2, + Primitive(self.tw.lc.prim_loop, + arg_descs=[ + ArgSlot(TYPE_OBJECT, + call_arg=False, + # TODO can we use controller_while in + # combination with not_? + wrapper=Primitive( + Primitive.controller_until, + arg_descs=[ArgSlot(TYPE_BOOL, + call_arg=False)])), + ArgSlot(TYPE_OBJECT)]), + True) - primitive_dictionary['clamp'] = self._prim_clamp palette.add_block('sandwichclamp', - hidden=True, style='clamp-style-collapsible', label=' ', special_name=_('top'), prim_name='clamp', help_string=_('top of a collapsible stack')) - self.tw.lc.def_prim('clamp', 1, primitive_dictionary['clamp'], True) + self.tw.lc.def_prim('clamp', 1, + Primitive(self.tw.lc.prim_clamp, + arg_descs=[ArgSlot(TYPE_OBJECT)]), + True) def _media_palette(self): - debug_output('creating %s palette' % _('media'), - self.tw.running_sugar) + palette = make_palette('media', colors=["#A0FF00", "#80A000"], help_string=_('Palette of media objects'), - position=7) + position=7, + translation=_('media')) palette.add_block('journal', style='box-style-media', @@ -136,7 +140,7 @@ boolean operators from Numbers palette')) default='None', special_name=_('journal'), help_string=_('Sugar Journal media object')) - PREFIX_DICTIONARY['journal'] = '#smedia_' + MEDIA_BLOCK2TYPE['journal'] = 'media' BLOCKS_WITH_SKIN.append('journal') MEDIA_SHAPES.append('journalsmall') MEDIA_SHAPES.append('journaloff') @@ -150,7 +154,7 @@ boolean operators from Numbers palette')) default='None', help_string=_('Sugar Journal audio object')) BLOCKS_WITH_SKIN.append('audio') - PREFIX_DICTIONARY['audio'] = '#saudio_' + MEDIA_BLOCK2TYPE['audio'] = 'audio' MEDIA_SHAPES.append('audiosmall') MEDIA_SHAPES.append('audiooff') MEDIA_SHAPES.append('audioon') @@ -163,7 +167,7 @@ boolean operators from Numbers palette')) default='None', help_string=_('Sugar Journal video object')) BLOCKS_WITH_SKIN.append('video') - PREFIX_DICTIONARY['video'] = '#svideo_' + MEDIA_BLOCK2TYPE['video'] = 'video' MEDIA_SHAPES.append('videosmall') MEDIA_SHAPES.append('videooff') MEDIA_SHAPES.append('videoon') @@ -176,7 +180,7 @@ boolean operators from Numbers palette')) default='None', help_string=_('Sugar Journal description field')) BLOCKS_WITH_SKIN.append('description') - PREFIX_DICTIONARY['description'] = '#sdescr_' + MEDIA_BLOCK2TYPE['description'] = 'descr' MEDIA_SHAPES.append('descriptionsmall') MEDIA_SHAPES.append('descriptionoff') MEDIA_SHAPES.append('descriptionon') @@ -188,7 +192,6 @@ boolean operators from Numbers palette')) special_name=_('text'), help_string=_('string value')) - primitive_dictionary['show'] = self._prim_show palette.add_block('show', style='basic-style-1arg', label=_('show'), @@ -198,8 +201,9 @@ boolean operators from Numbers palette')) help_string=_('draws text or show media from the \ Journal')) self.tw.lc.def_prim('show', 1, - lambda self, x: - primitive_dictionary['show'](x, True)) + Primitive(self.tw.lc.show, + arg_descs=[ArgSlot(TYPE_OBJECT), + ConstantArg(True)])) palette.add_block('showaligned', hidden=True, @@ -212,10 +216,10 @@ Journal')) help_string=_('draws text or show media from the \ Journal')) self.tw.lc.def_prim('showaligned', 1, - lambda self, x: - primitive_dictionary['show'](x, False)) + Primitive(self.tw.lc.show, + arg_descs=[ArgSlot(TYPE_OBJECT), + ConstantArg(False)])) - primitive_dictionary['setscale'] = self._prim_setscale palette.add_block('setscale', style='basic-style-1arg', label=_('set scale'), @@ -223,13 +227,14 @@ Journal')) default=33, logo_command='setlabelheight', help_string=_('sets the scale of media')) - self.tw.lc.def_prim('setscale', 1, - lambda self, x: - primitive_dictionary['setscale'](x)) + self.tw.lc.def_prim( + 'setscale', 1, + Primitive(self.tw.lc.set_scale, + arg_descs=[ArgSlot(TYPE_NUMBER)], + call_afterwards=lambda value: self.after_set( + 'scale', value))) - primitive_dictionary['savepix'] = self._prim_save_picture palette.add_block('savepix', - hidden=True, style='basic-style-1arg', label=_('save picture'), prim_name='savepix', @@ -237,11 +242,10 @@ Journal')) help_string=_('saves a picture to the Sugar \ Journal')) self.tw.lc.def_prim('savepix', 1, - lambda self, x: primitive_dictionary['savepix'](x)) + Primitive(self.tw.save_as_image, + arg_descs=[ArgSlot(TYPE_STRING)])) - primitive_dictionary['savesvg'] = self._prim_save_svg palette.add_block('savesvg', - hidden=True, style='basic-style-1arg', label=_('save SVG'), prim_name='savesvg', @@ -249,7 +253,9 @@ Journal')) help_string=_('saves turtle graphics as an SVG file \ in the Sugar Journal')) self.tw.lc.def_prim('savesvg', 1, - lambda self, x: primitive_dictionary['savesvg'](x)) + Primitive(self.tw.save_as_image, + arg_descs=[ArgSlot(TYPE_STRING)], + kwarg_descs={'svg': ConstantArg(True)})) palette.add_block('scale', style='box-style', @@ -258,7 +264,9 @@ in the Sugar Journal')) value_block=True, logo_command='labelsize', help_string=_('holds current scale value')) - self.tw.lc.def_prim('scale', 0, lambda self: self.tw.lc.scale) + self.tw.lc.def_prim('scale', 0, + Primitive(self.tw.lc.get_scale, + return_type=TYPE_NUMBER)) palette.add_block('mediawait', hidden=True, @@ -293,7 +301,6 @@ complete')) help_string=_('resume playing video or audio')) self.tw.lc.def_prim('mediaplay', 0, self.tw.lc.media_play, True) - primitive_dictionary['speak'] = self._prim_speak palette.add_block('speak', style='basic-style-1arg', label=_('speak'), @@ -301,9 +308,9 @@ complete')) default=_('hello'), help_string=_('speaks text')) self.tw.lc.def_prim('speak', 1, - lambda self, x: primitive_dictionary['speak'](x)) + Primitive(self.prim_speak, + arg_descs=[ArgSlot(TYPE_STRING)])) - primitive_dictionary['sinewave'] = self._prim_sinewave palette.add_block('sinewave', hidden=True, style='basic-style-3arg', @@ -315,23 +322,27 @@ complete')) help_string=_('plays a sinewave at frequency, \ amplitude, and duration (in seconds)')) self.tw.lc.def_prim('sinewave', 3, - lambda self, x, y, z: - primitive_dictionary['sinewave'](x, y, z)) + Primitive(self.prim_sinewave, + arg_descs=[ArgSlot(TYPE_NUMBER), + ArgSlot(TYPE_NUMBER), + ArgSlot(TYPE_NUMBER)])) def _sensor_palette(self): - debug_output('creating %s palette' % _('sensor'), - self.tw.running_sugar) + palette = make_palette('extras', colors=["#FF0000", "#A00000"], help_string=_('Palette of extra options'), - position=8) + position=8, + translation=_('extras')) + ''' palette = make_palette('sensor', colors=["#FF6060", "#A06060"], help_string=_('Palette of sensor blocks'), - position=6) + position=6, + translation=_('sensor')) ''' - primitive_dictionary['mousebutton'] = self._prim_mouse_button + palette.add_block('mousebutton', hidden=True, style='box-style', @@ -341,9 +352,9 @@ amplitude, and duration (in seconds)')) help_string=_('returns 1 if mouse button is \ pressed')) self.tw.lc.def_prim('mousebutton', 0, - lambda self: primitive_dictionary['mousebutton']()) + Primitive(self.tw.get_mouse_flag, + return_type=TYPE_NUMBER)) - primitive_dictionary['mousebutton2'] = self._prim_mouse_button_bool palette.add_block('mousebutton2', hidden=True, style='boolean-block-style', @@ -353,8 +364,8 @@ pressed')) help_string=_('returns True if mouse button is \ pressed')) self.tw.lc.def_prim('mousebutton2', 0, - lambda self: - primitive_dictionary['mousebutton2']()) + Primitive(self.tw.get_mouse_button, + return_type=TYPE_BOOL)) palette.add_block('mousex', hidden=True, @@ -364,8 +375,9 @@ pressed')) value_block=True, help_string=_('returns mouse x coordinate')) self.tw.lc.def_prim('mousex', 0, - lambda self: - self.tw.mouse_x - (self.tw.canvas.width / 2)) + Primitive(self.tw.get_mouse_x, + return_type=TYPE_NUMBER, + call_afterwards=self.after_mouse_x)) palette.add_block('mousey', hidden=True, @@ -375,10 +387,10 @@ pressed')) value_block=True, help_string=_('returns mouse y coordinate')) self.tw.lc.def_prim('mousey', 0, - lambda self: - (self.tw.canvas.height / 2) - self.tw.mouse_y) + Primitive(self.tw.get_mouse_y, + return_type=TYPE_NUMBER, + call_afterwards=self.after_mouse_y)) - primitive_dictionary['kbinput'] = self._prim_kbinput palette.add_block('kbinput', hidden=True, style='basic-style-extended-vertical', @@ -387,7 +399,8 @@ pressed')) help_string=_('query for keyboard input (results \ stored in keyboard block)')) self.tw.lc.def_prim('kbinput', 0, - lambda self: primitive_dictionary['kbinput']()) + Primitive(self.tw.get_keyboard_input, + call_afterwards=self.after_keypress)) palette.add_block('keyboard', hidden=True, @@ -398,34 +411,10 @@ stored in keyboard block)')) logo_command='make "keyboard readchar', help_string=_('holds results of query-keyboard \ block as ASCII')) - self.tw.lc.def_prim('keyboard', 0, lambda self: self.tw.lc.keyboard) - - ''' - palette.add_block('keyboard_chr', - style='box-style', - label='chr(%s)' % (_('keyboard')), - prim_name='keyboard_chr', - value_block=True, - logo_command='make "keyboard readchar', - help_string=_('holds results of query-keyboard \ -block as character')) - self.tw.lc.def_prim('keyboard_chr', 0, - lambda self: chr(self.tw.lc.keyboard)) - - primitive_dictionary['keyboardnum'] = self._prim_keyboard_num - palette.add_block('keyboard_num', - style='box-style', - label='num(%s)' % (_('keyboard')), - prim_name='keyboard_num', - value_block=True, - logo_command='make "keyboard readchar', - help_string=_('holds results of query-keyboard \ -block as number')) - self.tw.lc.def_prim('keyboard_num', 0, - lambda self: primitive_dictionary['keyboardnum']()) - ''' + self.tw.lc.def_prim('keyboard', 0, + Primitive(self.tw.get_keyboard, + return_type=TYPE_NUMBER)) - primitive_dictionary['readpixel'] = self._prim_readpixel palette.add_block('readpixel', hidden=True, style='basic-style-extended-vertical', @@ -435,20 +424,21 @@ block as number')) help_string=_('RGB color under the turtle is pushed \ to the stack')) self.tw.lc.def_prim('readpixel', 0, - lambda self: primitive_dictionary['readpixel']()) + Primitive(Turtle.read_pixel)) - primitive_dictionary['see'] = self._prim_see palette.add_block('see', hidden=True, style='box-style', label=_('turtle sees'), + value_block=True, prim_name='see', help_string=_('returns the color that the turtle \ "sees"')) self.tw.lc.def_prim('see', 0, - lambda self: primitive_dictionary['see']()) + Primitive(Turtle.get_color_index, + return_type=TYPE_NUMBER, + call_afterwards=self.after_see)) - primitive_dictionary['time'] = self._prim_time palette.add_block('time', hidden=True, style='box-style', @@ -457,18 +447,33 @@ to the stack')) value_block=True, help_string=_('elapsed time (in seconds) since \ program started')) - self.tw.lc.def_prim('time', 0, - lambda self: primitive_dictionary['time']()) + self.tw.lc.def_prim( + 'time', 0, + Primitive( + Primitive.identity, + return_type=TYPE_INT, + arg_descs=[ + ConstantArg( + Primitive( + int, + arg_descs=[ConstantArg( + Primitive(Primitive.minus, + arg_descs=[ + ConstantArg(Primitive(time)), + ConstantArg(Primitive( + self.tw.lc.get_start_time))]) + )] + ))], + call_afterwards=self.after_time)) def _extras_palette(self): - debug_output('creating %s palette' % _('extras'), - self.tw.running_sugar) + palette = make_palette('extras', colors=["#FF0000", "#A00000"], help_string=_('Palette of extra options'), - position=8) + position=8, + translation=_('extras')) - primitive_dictionary['push'] = self._prim_push palette.add_block('push', hidden=True, style='basic-style-1arg', @@ -478,12 +483,14 @@ program started')) logo_command='tapush', help_string=_('pushes value onto FILO (first-in \ last-out heap)')) - self.tw.lc.def_prim('push', 1, - lambda self, x: primitive_dictionary['push'](x)) + self.tw.lc.def_prim( + 'push', 1, + Primitive(self.tw.lc.heap.append, + arg_descs=[ArgSlot(TYPE_OBJECT)], + call_afterwards=self.after_push)) define_logo_function('tapush', 'to tapush :foo\nmake "taheap fput \ :foo :taheap\nend\nmake "taheap []\n') - primitive_dictionary['printheap'] = self._prim_printheap palette.add_block('printheap', hidden=True, style='basic-style-extended-vertical', @@ -492,12 +499,14 @@ last-out heap)')) logo_command='taprintheap', help_string=_('shows values in FILO (first-in \ last-out heap)')) - self.tw.lc.def_prim('printheap', 0, - lambda self: primitive_dictionary['printheap']()) + self.tw.lc.def_prim( + 'printheap', 0, + Primitive(self.tw.print_, + arg_descs=[ConstantArg(Primitive(self.tw.lc.get_heap)), + ConstantArg(False)])) define_logo_function('taprintheap', 'to taprintheap \nprint :taheap\n\ end\n') - primitive_dictionary['clearheap'] = self._prim_emptyheap palette.add_block('clearheap', hidden=True, style='basic-style-extended-vertical', @@ -506,12 +515,12 @@ end\n') logo_command='taclearheap', help_string=_('emptys FILO (first-in-last-out \ heap)')) - self.tw.lc.def_prim('clearheap', 0, - lambda self: primitive_dictionary['clearheap']()) + self.tw.lc.def_prim( + 'clearheap', 0, + Primitive(self.tw.lc.reset_heap, call_afterwards=self.after_pop)) define_logo_function('taclearheap', 'to taclearheap\nmake "taheap []\n\ end\n') - primitive_dictionary['pop'] = self._prim_pop palette.add_block('pop', hidden=True, style='box-style', @@ -522,12 +531,13 @@ end\n') logo_command='tapop', help_string=_('pops value off FILO (first-in \ last-out heap)')) - self.tw.lc.def_prim('pop', 0, - lambda self: primitive_dictionary['pop']()) + self.tw.lc.def_prim( + 'pop', 0, + Primitive(self.tw.lc.heap.pop, return_type=TYPE_BOX, + call_afterwards=self.after_pop)) define_logo_function('tapop', 'to tapop\nif emptyp :taheap [stop]\n\ make "tmp first :taheap\nmake "taheap butfirst :taheap\noutput :tmp\nend\n') - primitive_dictionary['isheapempty'] = self._prim_is_heap_empty palette.add_block('isheapempty', hidden=True, style='box-style', @@ -535,10 +545,59 @@ make "tmp first :taheap\nmake "taheap butfirst :taheap\noutput :tmp\nend\n') prim_name='isheapempty', value_block=True, help_string=_('returns True if heap is empty')) - self.tw.lc.def_prim('isheapempty', 0, - lambda self: primitive_dictionary['isheapempty']()) + self.tw.lc.def_prim( + 'isheapempty', 0, + Primitive(int, return_type=TYPE_INT, + arg_descs=[ConstantArg( + Primitive(Primitive.not_, return_type=TYPE_BOOL, + arg_descs=[ConstantArg( + Primitive(self.tw.lc.get_heap, + return_type=TYPE_BOOL))]))])) + + palette.add_block('saveheap', + hidden=True, + style='basic-style-1arg', + label=_('save heap to file'), + default=_('filename'), + prim_name='saveheap', + help_string=_('saves FILO (first-in \ +last-out heap) to a file')) + + self.tw.lc.def_prim('saveheap', 1, + Primitive(self.tw.lc.save_heap, + arg_descs=[ArgSlot(TYPE_OBJECT)])) + + if self.tw.running_sugar: + palette.add_block('loadheap', + hidden=True, + style='basic-style-1arg', + label=_('load heap from file'), + default=_('filename'), + prim_name='loadheap', + help_string=_('loads FILO (first-in \ +last-out heap) from a file')) + # macro + palette.add_block('loadheapfromjournal', + hidden=True, + style='basic-style-1arg', + label=_('load heap from file'), + help_string=_('loads FILO (first-in \ +last-out heap) from a file')) + else: + palette.add_block('loadheap', + hidden=True, + style='basic-style-1arg', + label=_('load heap from file'), + default=_('filename'), + prim_name='loadheap', + help_string=_('loads FILO (first-in \ +last-out heap) from a file')) + + self.tw.lc.def_prim('loadheap', 1, + Primitive(self.tw.lc.load_heap, + arg_descs=[ArgSlot(TYPE_OBJECT)], + call_afterwards=self.after_push)) - primitive_dictionary['isheapempty2'] = self._prim_is_heap_empty_bool palette.add_block('isheapempty2', hidden=True, style='boolean-block-style', @@ -546,11 +605,15 @@ make "tmp first :taheap\nmake "taheap butfirst :taheap\noutput :tmp\nend\n') prim_name='isheapempty2', value_block=True, help_string=_('returns True if heap is empty')) - self.tw.lc.def_prim('isheapempty2', 0, - lambda self: - primitive_dictionary['isheapempty2']()) + self.tw.lc.def_prim( + 'isheapempty2', 0, + # Python automatically converts the heap to a boolean in contexts + # where a boolean is needed + Primitive(Primitive.not_, return_type=TYPE_BOOL, + arg_descs=[ConstantArg( + Primitive(self.tw.lc.get_heap, + return_type=TYPE_BOOL))])) - primitive_dictionary['print'] = self._prim_print palette.add_block('comment', hidden=True, style='basic-style-1arg', @@ -559,9 +622,9 @@ make "tmp first :taheap\nmake "taheap butfirst :taheap\noutput :tmp\nend\n') default=_('comment'), string_or_number=True, help_string=_('places a comment in your code')) - self.tw.lc.def_prim('comment', 1, - lambda self, x: - primitive_dictionary['print'](x, True)) + self.tw.lc.def_prim( + 'comment', 1, + Primitive(Primitive.comment, arg_descs=[ArgSlot(TYPE_STRING)])) palette.add_block('print', style='basic-style-1arg', @@ -571,31 +634,44 @@ make "tmp first :taheap\nmake "taheap butfirst :taheap\noutput :tmp\nend\n') string_or_number=True, help_string=_('prints value in status block at \ bottom of the screen')) - self.tw.lc.def_prim('print', 1, - lambda self, x: - primitive_dictionary['print'](x, False)) + self.tw.lc.def_prim( + 'print', 1, + Primitive(self.tw.print_, + arg_descs=[ArgSlot(TYPE_OBJECT), ConstantArg(False)])) - primitive_dictionary['chr'] = self._prim_chr palette.add_block('chr', hidden=True, style='number-style-1arg', label='chr', prim_name='chr', help_string=_('Python chr operator')) - self.tw.lc.def_prim('chr', 1, - lambda self, x: primitive_dictionary['chr'](x)) + self.tw.lc.def_prim( + 'chr', 1, + Primitive(chr, return_type=TYPE_CHAR, + arg_descs=[ArgSlot(TYPE_INT)])) - primitive_dictionary['int'] = self._prim_int palette.add_block('int', hidden=True, style='number-style-1arg', label='int', prim_name='int', help_string=_('Python int operator')) - self.tw.lc.def_prim('int', 1, - lambda self, x: primitive_dictionary['int'](x)) + self.tw.lc.def_prim( + 'int', 1, + # leave over the actual work to the type system, and just demand + # that the argument be converted to an integer + Primitive(Primitive.identity, return_type=TYPE_INT, + arg_descs=[ArgSlot(TYPE_INT)])) + + palette.add_block('polar', + hidden=True, + style='basic-style-extended-vertical', + label=_('polar'), + prim_name='polar', + help_string=_('displays polar coordinates')) + self.tw.lc.def_prim('polar', 0, + lambda self: self.tw.set_polar(True)) - primitive_dictionary['myfunction'] = self._prim_myfunction palette.add_block('myfunc1arg', hidden=True, style='number-style-var-arg', @@ -605,9 +681,10 @@ bottom of the screen')) string_or_number=True, help_string=_('a programmable block: used to add \ advanced single-variable math equations, e.g., sin(x)')) - self.tw.lc.def_prim('myfunction', 2, - lambda self, f, x: - primitive_dictionary['myfunction'](f, [x])) + self.tw.lc.def_prim( + 'myfunction', 2, + Primitive(self.tw.lc.prim_myfunction, return_type=TYPE_FLOAT, + arg_descs=[ArgSlot(TYPE_STRING), ArgSlot(TYPE_FLOAT)])) palette.add_block('myfunc2arg', hidden=True, @@ -619,9 +696,11 @@ advanced single-variable math equations, e.g., sin(x)')) string_or_number=True, help_string=_('a programmable block: used to add \ advanced multi-variable math equations, e.g., sqrt(x*x+y*y)')) - self.tw.lc.def_prim('myfunction2', 3, - lambda self, f, x, y: - primitive_dictionary['myfunction'](f, [x, y])) + self.tw.lc.def_prim( + 'myfunction2', 3, + Primitive(self.tw.lc.prim_myfunction, return_type=TYPE_FLOAT, + arg_descs=[ArgSlot(TYPE_STRING), ArgSlot(TYPE_FLOAT), + ArgSlot(TYPE_FLOAT)])) palette.add_block('myfunc3arg', hidden=True, @@ -633,11 +712,21 @@ advanced multi-variable math equations, e.g., sqrt(x*x+y*y)')) string_or_number=True, help_string=_('a programmable block: used to add \ advanced multi-variable math equations, e.g., sin(x+y+z)')) - self.tw.lc.def_prim('myfunction3', 4, - lambda self, f, x, y, z: - primitive_dictionary['myfunction'](f, [x, y, z])) + self.tw.lc.def_prim( + 'myfunction3', 4, + Primitive(self.tw.lc.prim_myfunction, return_type=TYPE_FLOAT, + arg_descs=[ArgSlot(TYPE_STRING), ArgSlot(TYPE_FLOAT), + ArgSlot(TYPE_FLOAT), ArgSlot(TYPE_FLOAT)])) + + palette.add_block('cartesian', + hidden=True, + style='basic-style-extended-vertical', + label=_('Cartesian'), + prim_name='cartesian', + help_string=_('displays Cartesian coordinates')) + self.tw.lc.def_prim('cartesian', 0, + lambda self: self.tw.set_cartesian(True)) - primitive_dictionary['userdefined'] = self._prim_myblock palette.add_block('userdefined', hidden=True, style='basic-style-var-arg', @@ -649,8 +738,8 @@ advanced multi-variable math equations, e.g., sin(x+y+z)')) help_string=_('runs code found in the tamyblock.py \ module found in the Journal')) self.tw.lc.def_prim('userdefined', 1, - lambda self, x: - primitive_dictionary['userdefined']([x])) + Primitive(self.tw.lc.prim_myblock, + arg_descs=[ArgSlot(TYPE_OBJECT)])) BLOCKS_WITH_SKIN.append('userdefined') PYTHON_SKIN.append('userdefined') @@ -666,8 +755,9 @@ module found in the Journal')) help_string=_('runs code found in the tamyblock.py \ module found in the Journal')) self.tw.lc.def_prim('userdefined2', 2, - lambda self, x, y: - primitive_dictionary['userdefined']([x, y])) + Primitive(self.tw.lc.prim_myblock, + arg_descs=[ArgSlot(TYPE_OBJECT), + ArgSlot(TYPE_OBJECT)])) BLOCKS_WITH_SKIN.append('userdefined2args') PYTHON_SKIN.append('userdefined2args') @@ -683,31 +773,48 @@ module found in the Journal')) help_string=_('runs code found in the tamyblock.py \ module found in the Journal')) self.tw.lc.def_prim('userdefined3', 3, - lambda self, x, y, z: - primitive_dictionary['userdefined']([x, y, z])) + Primitive(self.tw.lc.prim_myblock, + arg_descs=[ArgSlot(TYPE_OBJECT), + ArgSlot(TYPE_OBJECT), + ArgSlot(TYPE_OBJECT)])) BLOCKS_WITH_SKIN.append('userdefined3args') PYTHON_SKIN.append('userdefined3args') MEDIA_SHAPES.append('pythonsmall') MEDIA_SHAPES.append('pythonoff') MEDIA_SHAPES.append('pythonon') - palette.add_block('cartesian', + palette.add_block('getfromurl', hidden=True, - style='basic-style-extended-vertical', - label=_('Cartesian'), - prim_name='cartesian', - help_string=_('displays Cartesian coordinates')) - self.tw.lc.def_prim('cartesian', 0, - lambda self: self.tw.set_cartesian(True)) + style='number-style-1arg', + #TRANS: URL is universal resource locator + label=_('URL'), + default=\ +'http://wiki.sugarlabs.org/images/2/2c/Logo_alt_3.svg', + prim_name='getfromurl', + help_string=\ +_('gets a text string or an image from a URL')) + self.tw.lc.def_prim('getfromurl', 1, + Primitive(self.tw.lc.get_from_url, + arg_descs=[ArgSlot(TYPE_STRING)])) - palette.add_block('polar', + + palette.add_block('skin', hidden=True, - style='basic-style-extended-vertical', - label=_('polar'), - prim_name='polar', - help_string=_('displays polar coordinates')) - self.tw.lc.def_prim('polar', 0, - lambda self: self.tw.set_polar(True)) + colors=["#FF0000", "#A00000"], + style='basic-style-1arg', + label=_('turtle shell'), + prim_name='skin', + help_string=_("put a custom 'shell' on the turtle")) + self.tw.lc.def_prim('skin', 1, + Primitive(self.tw.lc.reskin, + arg_descs=[ArgSlot(TYPE_OBJECT)])) + + # macro + palette.add_block('reskin', + hidden=True, + style='basic-style-1arg', + label=_('turtle shell'), + help_string=_("put a custom 'shell' on the turtle")) palette.add_block('addturtle', hidden=True, @@ -718,10 +825,9 @@ module found in the Journal')) string_or_number=True, help_string=_('chooses which turtle to command')) self.tw.lc.def_prim('addturtle', 1, - lambda self, x: - self.tw.turtles.set_turtle(x)) + Primitive(self.tw.lc.prim_turtle, + arg_descs=[ArgSlot(TYPE_STRING)])) - primitive_dictionary['turtlex'] = self._prim_turtle_x palette.add_block('turtlex', hidden=True, style='number-style-1arg', @@ -729,10 +835,12 @@ module found in the Journal')) prim_name='turtlex', default=['Yertle'], help_string=_('Returns x coordinate of turtle')) - self.tw.lc.def_prim('turtlex', 1, - lambda self, t: primitive_dictionary['turtlex'](t)) + self.tw.lc.def_prim( + 'turtlex', 1, + Primitive(self.tw.turtles.get_turtle_x, + arg_descs=[ArgSlot(TYPE_OBJECT)], + return_type=TYPE_BOX)) - primitive_dictionary['turtley'] = self._prim_turtle_y palette.add_block('turtley', hidden=True, style='number-style-1arg', @@ -740,23 +848,24 @@ module found in the Journal')) prim_name='turtley', default=['Yertle'], help_string=_('Returns y coordinate of turtle')) - self.tw.lc.def_prim('turtley', 1, - lambda self, t: primitive_dictionary['turtley'](t)) + self.tw.lc.def_prim( + 'turtley', 1, + Primitive(self.tw.turtles.get_turtle_y, + arg_descs=[ArgSlot(TYPE_OBJECT)], + return_type=TYPE_BOX)) - primitive_dictionary['activeturtle'] = self._prim_active_turtle palette.add_block('activeturtle', hidden=True, style='box-style', - #TRANS: pop removes a new item from the program stack label=_('active turtle'), prim_name='activeturtle', value_block=True, help_string=_('the name of the active turtle')) - self.tw.lc.def_prim('activeturtle', 0, - lambda self: - primitive_dictionary['activeturtle']()) + self.tw.lc.def_prim( + 'activeturtle', 0, + Primitive(Turtle.get_name, + return_type=TYPE_BOX)) - primitive_dictionary['turtleh'] = self._prim_turtle_h palette.add_block('turtleh', hidden=True, style='number-style-1arg', @@ -764,27 +873,11 @@ module found in the Journal')) prim_name='turtleh', default=['Yertle'], help_string=_('Returns heading of turtle')) - self.tw.lc.def_prim('turtleh', 1, - lambda self, t: primitive_dictionary['turtleh'](t)) - - primitive_dictionary['skin'] = self._prim_reskin - palette.add_block('skin', - hidden=True, - colors=["#FF0000", "#A00000"], - style='basic-style-1arg', - label=_('turtle shell'), - prim_name='skin', - help_string=_("put a custom 'shell' on the turtle")) - self.tw.lc.def_prim('skin', 1, - lambda self, x: - primitive_dictionary['skin'](x)) - - # macro - palette.add_block('reskin', - hidden=True, - style='basic-style-1arg', - label=_('turtle shell'), - help_string=_("put a custom 'shell' on the turtle")) + self.tw.lc.def_prim( + 'turtleh', 1, + Primitive(self.tw.turtles.get_turtle_heading, + arg_descs=[ArgSlot(TYPE_OBJECT)], + return_type=TYPE_BOX)) palette.add_block('sandwichclampcollapsed', hidden=True, @@ -794,7 +887,19 @@ module found in the Journal')) special_name=_('top'), help_string=_('top of a collapsed stack')) - primitive_dictionary['loadblock'] = self._prim_load_block + palette.add_block('loadpalette', + hidden=True, + style='basic-style-1arg', + string_or_number=True, + label=_('select palette'), + prim_name='loadpalette', + default=_('turtle'), + help_string=_('selects a palette')) + self.tw.lc.def_prim('loadpalette', 1, + Primitive(self.tw.prim_load_palette, + export_me=False, + arg_descs=[ArgSlot(TYPE_STRING)])) + palette.add_block('loadblock', hidden=True, style='basic-style-var-arg', @@ -803,8 +908,9 @@ module found in the Journal')) default=_('forward'), help_string=_('loads a block')) self.tw.lc.def_prim('loadblock', 1, - lambda self, x: - primitive_dictionary['loadblock'](x)) + Primitive(self.tw.prim_load_block, + export_me=False, + arg_descs=[ArgSlot(TYPE_STRING)])) palette.add_block('loadblock2arg', style='basic-style-var-arg', @@ -815,8 +921,10 @@ module found in the Journal')) default=[_('forward'), 100], help_string=_('loads a block')) self.tw.lc.def_prim('loadblock2', 2, - lambda self, x, y: - primitive_dictionary['loadblock']([x, y])) + Primitive(self.tw.prim_load_block, + export_me=False, + arg_descs=[ArgSlot(TYPE_STRING), + ArgSlot(TYPE_OBJECT)])) palette.add_block('loadblock3arg', style='basic-style-var-arg', @@ -827,56 +935,47 @@ module found in the Journal')) default=[_('setxy'), 0, 0], help_string=_('loads a block')) self.tw.lc.def_prim('loadblock3', 3, - lambda self, x, y, z: - primitive_dictionary['loadblock']([x, y, z])) - - primitive_dictionary['loadpalette'] = self._prim_load_palette - palette.add_block('loadpalette', - hidden=True, - style='basic-style-1arg', - string_or_number=True, - label=_('select palette'), - prim_name='loadpalette', - default=_('turtle'), - help_string=_('selects a palette')) - self.tw.lc.def_prim('loadpalette', 1, - lambda self, x: - primitive_dictionary['loadpalette'](x)) + Primitive(self.tw.prim_load_block, + export_me=False, + arg_descs=[ArgSlot(TYPE_STRING), + ArgSlot(TYPE_OBJECT), + ArgSlot(TYPE_OBJECT)])) def _portfolio_palette(self): - debug_output('creating %s palette' % _('portfolio'), - self.tw.running_sugar) + palette = make_palette('extras', colors=["#FF0000", "#A00000"], help_string=_('Palette of extra options'), - position=8) + position=8, + translation=_('extras')) ''' palette = make_palette('portfolio', colors=["#0606FF", "#0606A0"], help_string=_('Palette of presentation \ templates'), - position=9) + position=9, + translation=_('portfolio')) ''' - primitive_dictionary['hideblocks'] = self._prim_hideblocks palette.add_block('hideblocks', hidden=True, style='basic-style-extended-vertical', label=_('hide blocks'), prim_name='hideblocks', help_string=_('declutters canvas by hiding blocks')) - self.tw.lc.def_prim('hideblocks', 0, - lambda self: primitive_dictionary['hideblocks']()) + self.tw.lc.def_prim( + 'hideblocks', 0, + Primitive(self._prim_hideblocks, export_me=False)) - primitive_dictionary['showblocks'] = self._prim_showblocks palette.add_block('showblocks', hidden=True, style='basic-style-extended-vertical', label=_('show blocks'), prim_name='showblocks', help_string=_('restores hidden blocks')) - self.tw.lc.def_prim('showblocks', 0, - lambda self: primitive_dictionary['showblocks']()) + self.tw.lc.def_prim( + 'showblocks', 0, + Primitive(self._prim_showblocks, export_me=False)) palette.add_block('fullscreen', hidden=True, @@ -884,8 +983,9 @@ templates'), label=_('Fullscreen').lower(), prim_name='fullscreen', help_string=_('hides the Sugar toolbars')) - self.tw.lc.def_prim('fullscreen', 0, - lambda self: self.tw.set_fullscreen()) + self.tw.lc.def_prim( + 'fullscreen', 0, + Primitive(self.tw.set_fullscreen, export_me=False)) primitive_dictionary['bulletlist'] = self._prim_list palette.add_block('list', @@ -956,7 +1056,10 @@ Journal objects')) prim_name='lpos', logo_command='lpos', help_string=_('xcor of left of screen')) - self.tw.lc.def_prim('lpos', 0, lambda self: CONSTANTS['leftpos']) + self.tw.lc.def_prim( + 'lpos', 0, + Primitive(CONSTANTS.get, return_type=TYPE_INT, + arg_descs=[ConstantArg('leftpos')])) palette.add_block('bottompos', style='box-style', @@ -964,7 +1067,10 @@ Journal objects')) prim_name='bpos', logo_command='bpos', help_string=_('ycor of bottom of screen')) - self.tw.lc.def_prim('bpos', 0, lambda self: CONSTANTS['bottompos']) + self.tw.lc.def_prim( + 'bpos', 0, + Primitive(CONSTANTS.get, return_type=TYPE_INT, + arg_descs=[ConstantArg('bottompos')])) palette.add_block('width', style='box-style', @@ -972,7 +1078,10 @@ Journal objects')) prim_name='hres', logo_command='width', help_string=_('the canvas width')) - self.tw.lc.def_prim('hres', 0, lambda self: CONSTANTS['width']) + self.tw.lc.def_prim( + 'hres', 0, + Primitive(CONSTANTS.get, return_type=TYPE_INT, + arg_descs=[ConstantArg('width')])) palette.add_block('rightpos', style='box-style', @@ -980,7 +1089,10 @@ Journal objects')) prim_name='rpos', logo_command='rpos', help_string=_('xcor of right of screen')) - self.tw.lc.def_prim('rpos', 0, lambda self: CONSTANTS['rightpos']) + self.tw.lc.def_prim( + 'rpos', 0, + Primitive(CONSTANTS.get, return_type=TYPE_INT, + arg_descs=[ConstantArg('rightpos')])) palette.add_block('toppos', style='box-style', @@ -988,7 +1100,10 @@ Journal objects')) prim_name='tpos', logo_command='tpos', help_string=_('ycor of top of screen')) - self.tw.lc.def_prim('tpos', 0, lambda self: CONSTANTS['toppos']) + self.tw.lc.def_prim( + 'tpos', 0, + Primitive(CONSTANTS.get, return_type=TYPE_INT, + arg_descs=[ConstantArg('toppos')])) palette.add_block('height', style='box-style', @@ -996,7 +1111,10 @@ Journal objects')) prim_name='vres', logo_command='height', help_string=_('the canvas height')) - self.tw.lc.def_prim('vres', 0, lambda self: CONSTANTS['height']) + self.tw.lc.def_prim( + 'vres', 0, + Primitive(CONSTANTS.get, return_type=TYPE_INT, + arg_descs=[ConstantArg('height')])) palette.add_block('titlex', hidden=True, @@ -1005,7 +1123,10 @@ Journal objects')) label=_('title x'), logo_command='titlex', prim_name='titlex') - self.tw.lc.def_prim('titlex', 0, lambda self: CONSTANTS['titlex']) + self.tw.lc.def_prim( + 'titlex', 0, + Primitive(CONSTANTS.get, return_type=TYPE_INT, + arg_descs=[ConstantArg('titlex')])) palette.add_block('titley', hidden=True, @@ -1014,7 +1135,10 @@ Journal objects')) label=_('title y'), logo_command='titley', prim_name='titley') - self.tw.lc.def_prim('titley', 0, lambda self: CONSTANTS['titley']) + self.tw.lc.def_prim( + 'titley', 0, + Primitive(CONSTANTS.get, return_type=TYPE_INT, + arg_descs=[ConstantArg('titley')])) palette.add_block('leftx', hidden=True, @@ -1023,7 +1147,10 @@ Journal objects')) label=_('left x'), prim_name='leftx', logo_command='leftx') - self.tw.lc.def_prim('leftx', 0, lambda self: CONSTANTS['leftx']) + self.tw.lc.def_prim( + 'leftx', 0, + Primitive(CONSTANTS.get, return_type=TYPE_INT, + arg_descs=[ConstantArg('leftx')])) palette.add_block('topy', hidden=True, @@ -1032,7 +1159,10 @@ Journal objects')) label=_('top y'), prim_name='topy', logo_command='topy') - self.tw.lc.def_prim('topy', 0, lambda self: CONSTANTS['topy']) + self.tw.lc.def_prim( + 'topy', 0, + Primitive(CONSTANTS.get, return_type=TYPE_INT, + arg_descs=[ConstantArg('topy')])) palette.add_block('rightx', hidden=True, @@ -1041,7 +1171,10 @@ Journal objects')) label=_('right x'), prim_name='rightx', logo_command='rightx') - self.tw.lc.def_prim('rightx', 0, lambda self: CONSTANTS['rightx']) + self.tw.lc.def_prim( + 'rightx', 0, + Primitive(CONSTANTS.get, return_type=TYPE_INT, + arg_descs=[ConstantArg('rightx')])) palette.add_block('bottomy', hidden=True, @@ -1050,7 +1183,10 @@ Journal objects')) label=_('bottom y'), prim_name='bottomy', logo_command='bottomy') - self.tw.lc.def_prim('bottomy', 0, lambda self: CONSTANTS['bottomy']) + self.tw.lc.def_prim( + 'bottomy', 0, + Primitive(CONSTANTS.get, return_type=TYPE_INT, + arg_descs=[ConstantArg('bottomy')])) def _myblocks_palette(self): ''' User-defined macros are saved as a json-encoded file; @@ -1060,12 +1196,11 @@ Journal objects')) os.path.exists(self.tw.macros_path): files = glob.glob(os.path.join(self.tw.macros_path, '*.tb')) if len(files) > 0: - debug_output('creating %s palette' % _('my blocks'), - self.tw.running_sugar) palette = make_palette( - 'my blocks', + 'myblocks', colors=["#FFFF00", "#A0A000"], - help_string=_('Palette of user-defined operators')) + help_string=_('Palette of user-defined operators'), + translation=_('my blocks')) for tafile in files: data = data_from_file(tafile) @@ -1078,235 +1213,36 @@ Journal objects')) # Block primitives - def _prim_emptyheap(self): - """ Empty FILO """ - self.tw.lc.heap = [] - - def _prim_kbinput(self): - """ Query keyboard """ - if len(self.tw.keypress) == 1: - self.tw.lc.keyboard = ord(self.tw.keypress[0]) - else: - try: - self.tw.lc.keyboard = { - 'Escape': 27, 'space': 32, ' ': 32, - 'Return': 13, 'KP_Up': 2, 'KP_Down': 4, 'KP_Left': 1, - 'KP_Right': 3}[self.tw.keypress] - except KeyError: - self.tw.lc.keyboard = 0 + def after_keypress(self): if self.tw.lc.update_values: - self.tw.lc.update_label_value('keyboard', self.tw.lc.keyboard) - self.tw.keypress = '' - - def _prim_list(self, blklist): - """ Expandable list block """ - self._prim_showlist(blklist) - self.tw.lc.ireturn() - yield True - - def _prim_myblock(self, x): - """ Run Python code imported from Journal """ - if self.tw.lc.bindex is not None and \ - self.tw.lc.bindex in self.tw.myblock: - try: - if len(x) == 1: - myfunc_import(self, self.tw.myblock[self.tw.lc.bindex], - x[0]) + if self.tw.keypress in KEY_DICT: + if KEY_DICT[self.tw.keypress] in REVERSE_KEY_DICT: + self.tw.lc.update_label_value( + 'keyboard', REVERSE_KEY_DICT[ + KEY_DICT[self.tw.keypress]]) else: - myfunc_import(self, self.tw.myblock[self.tw.lc.bindex], x) - except: - raise logoerror("#syntaxerror") - - def _prim_myfunction(self, f, x): - """ Programmable block """ - for i, v in enumerate(x): - if type(v) == int: # Pass float values to Python block - x[i] = float(v) - try: - y = myfunc(f, x) - if str(y) == 'nan': - debug_output('Python function returned NAN', - self.tw.running_sugar) - self.tw.lc.stop_logo() - raise logoerror("#notanumber") - else: - return y - except ZeroDivisionError: - self.tw.lc.stop_logo() - raise logoerror("#zerodivide") - except ValueError, e: - self.tw.lc.stop_logo() - raise logoerror('#' + str(e)) - except SyntaxError, e: - self.tw.lc.stop_logo() - raise logoerror('#' + str(e)) - except NameError, e: - self.tw.lc.stop_logo() - raise logoerror('#' + str(e)) - except OverflowError: - self.tw.lc.stop_logo() - raise logoerror("#overflowerror") - except TypeError: - self.tw.lc.stop_logo() - raise logoerror("#notanumber") - - def _prim_is_heap_empty(self): - """ is FILO empty? """ - if len(self.tw.lc.heap) == 0: - return 1 - else: - return 0 - - def _prim_is_heap_empty_bool(self): - """ is FILO empty? """ - if len(self.tw.lc.heap) == 0: - return True - else: - return False + self.tw.lc.update_label_value( + 'keyboard', chr(KEY_DICT[self.tw.keypress])) + elif self.tw.keyboard > 0: + self.tw.lc.update_label_value('keyboard', + chr(self.tw.keyboard)) + self.tw.keypress = '' - def _prim_pop(self): - """ Pop value off of FILO """ - if len(self.tw.lc.heap) == 0: - raise logoerror("#emptyheap") - else: - if self.tw.lc.update_values: - if len(self.tw.lc.heap) == 1: - self.tw.lc.update_label_value('pop') - else: - self.tw.lc.update_label_value('pop', self.tw.lc.heap[-2]) - return self.tw.lc.heap.pop(-1) - - def _prim_print(self, n, flag): - """ Print object n """ - if flag and (self.tw.hide or self.tw.step_time == 0): - return - if type(n) == list: - self.tw.showlabel('print', n) - elif type(n) == str or type(n) == unicode: - if n in COLORDICT: - if COLORDICT[n][0] is None: - self.tw.showlabel('print', '%s %d, %s %d' % - (_('shade'), COLORDICT[n][1], - _('gray'), COLORDICT[n][2])) - else: - self.tw.showlabel('print', '%s %d, %s %d, %s %d' % - (_('color'), COLORDICT[n][0], - _('shade'), COLORDICT[n][1], - _('gray'), COLORDICT[n][2])) - elif n[0:6] == 'media_' and \ - n[6:].lower not in media_blocks_dictionary: - try: - if self.tw.running_sugar: - from sugar.datastore import datastore - try: - dsobject = datastore.get(n[6:]) - except: - debug_output("Couldn't open %s" % (n[6:]), - self.tw.running_sugar) - self.tw.showlabel('print', dsobject.metadata['title']) - dsobject.destroy() - else: - self.tw.showlabel('print', n[6:]) - except IOError: - self.tw.showlabel('print', n) + def after_pop(self, *ignored_args): + if self.tw.lc.update_values: + if not self.tw.lc.heap: + self.tw.lc.update_label_value('pop') else: - self.tw.showlabel('print', n) - elif type(n) == int: - self.tw.showlabel('print', n) - else: - self.tw.showlabel( - 'print', - str(round_int(n)).replace('.', self.tw.decimal_point)) - - def _prim_printheap(self): - """ Display contents of heap """ - heap_as_string = str(self.tw.lc.heap) - if len(heap_as_string) > 80: - self.tw.showlabel('print', str(self.tw.lc.heap)[0:79] + '…') - else: - self.tw.showlabel('print', str(self.tw.lc.heap)) + self.tw.lc.update_label_value('pop', self.tw.lc.heap[-1]) - def _prim_push(self, val): - """ Push value onto FILO """ - self.tw.lc.heap.append(val) + def after_push(self, *ignored_args): if self.tw.lc.update_values: - self.tw.lc.update_label_value('pop', val) - - def _prim_readpixel(self): - """ Read r, g, b, a from the canvas and push b, g, r to the stack """ - r, g, b, a = self.tw.turtles.get_active_turtle().get_pixel() - self.tw.lc.heap.append(b) - self.tw.lc.heap.append(g) - self.tw.lc.heap.append(r) - - def _prim_active_turtle(self): - return(self.tw.turtles.get_active_turtle().get_name()) - - def _prim_reskin(self, media): - """ Reskin the turtle with an image from a file """ - scale = int(ICON_SIZE * float(self.tw.lc.scale) / DEFAULT_SCALE) - if scale < 1: - return - self.tw.lc.filepath = None - dsobject = None - if os.path.exists(media[6:]): # is it a path? - self.tw.lc.filepath = media[6:] - elif self.tw.running_sugar: # is it a datastore object? - from sugar.datastore import datastore - try: - dsobject = datastore.get(media[6:]) - except: - debug_output("Couldn't open skin %s" % (media[6:]), - self.tw.running_sugar) - if dsobject is not None: - self.tw.lc.filepath = dsobject.file_path - if self.tw.lc.filepath is None: - self.tw.showlabel('nojournal', self.tw.lc.filepath) - return - pixbuf = None - try: - pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(self.tw.lc.filepath, - scale, scale) - except: - self.tw.showlabel('nojournal', self.tw.lc.filepath) - debug_output("Couldn't open skin %s" % (self.tw.lc.filepath), - self.tw.running_sugar) - if pixbuf is not None: - self.tw.turtles.get_active_turtle().set_shapes([pixbuf]) - pen_state = self.tw.turtles.get_active_turtle().get_pen_state() - if pen_state: - self.tw.turtles.get_active_turtle().set_pen_state(False) - self.tw.turtles.get_active_turtle().forward(0) - if pen_state: - self.tw.turtles.get_active_turtle().set_pen_state(True) - - if self.tw.sharing(): - if self.tw.running_sugar: - tmp_path = get_path(self.tw.activity, 'instance') + if not self.tw.lc.heap: + self.tw.lc.update_label_value('pop') else: - tmp_path = '/tmp' - tmp_file = os.path.join(get_path(self.tw.activity, 'instance'), - 'tmpfile.png') - pixbuf.save(tmp_file, 'png', {'quality': '100'}) - data = image_to_base64(tmp_file, tmp_path) - height = pixbuf.get_height() - width = pixbuf.get_width() - event = 'R|%s' % (data_to_string([self.tw.nick, - [round_int(width), - round_int(height), - data]])) - gobject.idle_add(self.tw.send_event, event) - os.remove(tmp_file) - - def _prim_save_picture(self, name): - """ Save canvas to file as PNG """ - self.tw.save_as_image(name) - - def _prim_save_svg(self, name): - """ Save SVG to file """ - self.tw.save_as_image(name, svg=True) - - def _prim_speak(self, text): + self.tw.lc.update_label_value('pop', self.tw.lc.heap[-1]) + + def prim_speak(self, text): """ Speak text """ if type(text) == float and int(text) == text: text = int(text) @@ -1326,7 +1262,7 @@ Journal objects')) language_option, text])) self.tw.send_event(event) - def _prim_sinewave(self, pitch, amplitude, duration): + def prim_sinewave(self, pitch, amplitude, duration): """ Create a Csound score to play a sine wave. """ self.orchlines = [] self.scorelines = [] @@ -1400,125 +1336,34 @@ Journal objects')) csd.write("\n") csd.close() - def _prim_mouse_button(self): - """ Return 1 if mouse button is pressed """ - if self.tw.mouse_flag == 1: - return 1 - else: - return 0 - - def _prim_mouse_button_bool(self): - """ Return True if mouse button is pressed """ - if self.tw.mouse_flag == 1: - return True - else: - return False + def after_mouse_x(self): + """ Show mouse x coordinate """ + if self.tw.lc.update_values: + self.tw.lc.update_label_value('mousex', self.tw.get_mouse_x()) - def _prim_see(self): - """ Read r, g, b from the canvas and return a corresponding palette - color """ - r, g, b, a = self.tw.turtles.get_active_turtle().get_pixel() - color_index = self.tw.canvas.get_color_index(r, g, b) + def after_mouse_y(self): + """ Show mouse y coordinate """ if self.tw.lc.update_values: - self.tw.lc.update_label_value('see', color_index) - return color_index + self.tw.lc.update_label_value('mousey', self.tw.get_mouse_y()) - def _prim_setscale(self, scale): - """ Set the scale used by the show block """ - self.tw.lc.scale = scale + def after_see(self): + """ Show color under turtle """ if self.tw.lc.update_values: - self.tw.lc.update_label_value('scale', scale) - - def _prim_show(self, string, center=False): - """ Show is the general-purpose media-rendering block. """ - if type(string) == str or type(string) == unicode: - if string in ['media_', 'descr_', 'audio_', 'video_', - 'media_None', 'descr_None', 'audio_None', - 'video_None']: - pass - elif string[0:6] in ['media_', 'descr_', 'audio_', 'video_']: - self.tw.lc.filepath = None - self.tw.lc.pixbuf = None # Camera writes directly to pixbuf - self.tw.lc.dsobject = None - if string[6:].lower() in media_blocks_dictionary: - media_blocks_dictionary[string[6:].lower()]() - elif os.path.exists(string[6:]): # is it a path? - self.tw.lc.filepath = string[6:] - elif self.tw.running_sugar: # is it a datastore object? - from sugar.datastore import datastore - try: - self.tw.lc.dsobject = datastore.get(string[6:]) - except: - debug_output("Couldn't find dsobject %s" % - (string[6:]), self.tw.running_sugar) - if self.tw.lc.dsobject is not None: - self.tw.lc.filepath = self.tw.lc.dsobject.file_path - if self.tw.lc.pixbuf is not None: - self.tw.lc.insert_image(center=center, pixbuf=True) - elif self.tw.lc.filepath is None: - if self.tw.lc.dsobject is not None: - self.tw.showlabel( - 'nojournal', - self.tw.lc.dsobject.metadata['title']) - else: - self.tw.showlabel('nojournal', string[6:]) - debug_output("Couldn't open %s" % (string[6:]), - self.tw.running_sugar) - elif string[0:6] == 'media_': - self.tw.lc.insert_image(center=center) - elif string[0:6] == 'descr_': - mimetype = None - if self.tw.lc.dsobject is not None and \ - 'mime_type' in self.tw.lc.dsobject.metadata: - mimetype = self.tw.lc.dsobject.metadata['mime_type'] - description = None - if self.tw.lc.dsobject is not None and \ - 'description' in self.tw.lc.dsobject.metadata: - description = self.tw.lc.dsobject.metadata[ - 'description'] - self.tw.lc.insert_desc(mimetype, description) - elif string[0:6] == 'audio_': - self.tw.lc.play_sound() - elif string[0:6] == 'video_': - self.tw.lc.play_video() - if self.tw.lc.dsobject is not None: - self.tw.lc.dsobject.destroy() - else: # assume it is text to display - x, y = self.tw.lc.x2tx(), self.tw.lc.y2ty() - if center: - y -= self.tw.canvas.textsize - self.tw.turtles.get_active_turtle().draw_text(string, x, y, - int(self.tw.canvas.textsize * - self.tw.lc.scale / 100.), - self.tw.canvas.width - x) - elif type(string) == float or type(string) == int: - string = round_int(string) - x, y = self.tw.lc.x2tx(), self.tw.lc.y2ty() - if center: - y -= self.tw.canvas.textsize - self.tw.turtles.get_active_turtle().draw_text(string, x, y, - int(self.tw.canvas.textsize * - self.tw.lc.scale / 100.), - self.tw.canvas.width - x) - - def _prim_showlist(self, sarray): - """ Display list of media objects """ - x = (self.tw.turtles.get_active_turtle().get_xy()[0] / - self.tw.coord_scale) - y = (self.tw.turtles.get_active_turtle().get_xy()[1] / - self.tw.coord_scale) - for s in sarray: - self.tw.turtles.get_active_turtle().set_xy(x, y, pendown=False) - self._prim_show(s) - y -= int(self.tw.canvas.textsize * self.tw.lead) - - def _prim_time(self): - """ Number of seconds since program execution has started or - clean (prim_clear) block encountered """ - elapsed_time = int(time() - self.tw.lc.start_time) + self.tw.lc.update_label_value( + 'see', + self.tw.turtles.get_active_turtle().get_color_index()) + + def _prim_list(self, blklist): + """ Expandable list block """ + self.tw.lc.showlist(blklist) + self.tw.lc.ireturn() + yield True + + def after_time(self, elapsed_time): + """ Update the label of the 'time' block after computing the new + value. """ if self.tw.lc.update_values: self.tw.lc.update_label_value('time', elapsed_time) - return elapsed_time def _prim_hideblocks(self): """ hide blocks and show showblocks button """ @@ -1538,137 +1383,8 @@ Journal objects')) self.tw.activity.stop_turtle_button.set_icon("stopiton") self.tw.activity.stop_turtle_button.set_tooltip(_('Stop turtle')) - def _prim_chr(self, x): - """ Chr conversion """ - try: - return chr(int(x)) - except ValueError: - self.tw.lc.stop_logo() - raise logoerror("#notanumber") - - def _prim_int(self, x): - """ Int conversion """ - try: - return int(x) - except ValueError: - self.tw.lc.stop_logo() - raise logoerror("#notanumber") - - def _prim_turtle_x(self, t): - """ Return x coordinate of turtle t """ - return self.tw.turtles.get_turtle_x(t) - - def _prim_turtle_y(self, t): - """ Return y coordinate of turtle t """ - return self.tw.turtles.get_turtle_y(t) - - def _prim_turtle_h(self, t): - """ Return heading of turtle t """ - return self.tw.turtles.get_turtle_heading(t) - - def _prim_clamp(self, blklist): - """ Run clamp blklist """ - self.tw.lc.icall(self.tw.lc.evline, blklist[:]) - yield True - self.tw.lc.procstop = False - self.tw.lc.ireturn() - yield True - - def _prim_load_block(self, blkname): - ''' Load a block on to the canvas ''' - # Place the block at the active turtle (x, y) and move the turtle - # into position to place the next block in the stack. - # TODO: Add expandable argument - pos = self.tw.turtles.get_active_turtle().get_xy() - if isinstance(blkname, list): - name = blkname[0] - if len(blkname) > 1: - value = blkname[1:] - dy = int(self._find_block(name, pos[0], pos[1], value)) - else: - dy = int(self._find_block(name, pos[0], pos[1])) - else: - name = blkname - if name == 'delete': - for blk in self.tw.just_blocks(): - if blk.status == 'load block': - blk.type = 'trash' - blk.spr.hide() - dy = 0 - else: - dy = int(self._find_block(name, pos[0], pos[1])) - - # Reposition turtle to end of flow - pos = self.tw.turtles.get_active_turtle().get_xy() - pos[1] -= dy - self.tw.turtles.get_active_turtle().move_turtle(pos) - - def _make_block(self, name, x, y, defaults): - if defaults is None: - self.tw._new_block(name, x, y, defaults) - else: - for i, v in enumerate(defaults): - if type(v) == float and int(v) == v: - defaults[i] = int(v) - self.tw._new_block(name, x, y, defaults) - - # Find the block we just created and attach it to a stack. - self.tw.drag_group = None - spr = self.tw.sprite_list.find_sprite((x, y)) - if spr is not None: - blk = self.tw.block_list.spr_to_block(spr) - if blk is not None: - self.tw.drag_group = find_group(blk) - for b in self.tw.drag_group: - b.status = 'load block' - self.tw._snap_to_dock() - - # Disassociate new block from mouse. - self.tw.drag_group = None - return blk.docks[-1][3] - - def _find_block(self, blkname, x, y, defaults=None): - """ Create a new block. It is a bit more work than just calling - _new_block(). We need to: - (1) translate the label name into the internal block name; - (2) 'dock' the block onto a stack where appropriate; and - (3) disassociate the new block from the mouse. """ - x, y = self.tw.turtles.turtle_to_screen_coordinates((x, y)) - for name in block_names: - # Translate label name into block/prim name. - if blkname in block_names[name]: # block label is an array - # print 'found a match', blkname, name, block_names[name] - if name in content_blocks or \ - (name in block_primitives and - block_primitives[name] == name): - # print '_make_block', blkname, name - return self._make_block(name, x, y, defaults) - elif blkname in block_names: - # print '_make_block', blkname - return self._make_block(blkname, x, y, defaults) - for name in special_names: - # Translate label name into block/prim name. - if blkname in special_names[name]: - return self._make_block(name, x, y, defaults) - # Check for a macro - if blkname in MACROS: - self.tw.new_macro(blkname, x, y) - return 0 # Fix me: calculate flow position - # Block not found - raise logoerror("#syntaxerror") - return -1 - - def _prim_load_palette(self, arg): - ''' Select a palette ''' - if type(arg) in [int, float]: - if int(arg) < 0 or int(arg) > len(palette_names): - raise logoerror("#syntaxerror") - else: - self.tw.show_toolbar_palette(int(arg)) - else: - if type(arg) == unicode: - arg = arg.encode('utf-8') - if arg in palette_names or arg in palette_i18n_names: - self.tw.show_toolbar_palette(palette_name_to_index(arg)) - else: - raise logoerror("#syntaxerror") + def after_set(self, name, value=None): + ''' Update the associated value blocks ''' + if value is not None: + if self.tw.lc.update_values: + self.tw.lc.update_label_value(name, value) diff --git a/pyexported/__init__.py b/pyexported/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pyexported/__init__.py diff --git a/pyexported/window_setup.py b/pyexported/window_setup.py new file mode 100644 index 0000000..537022a --- /dev/null +++ b/pyexported/window_setup.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python + +import cairo +import pygtk +pygtk.require('2.0') +import gtk +import gobject + +from gettext import gettext as _ + +import os +import sys + +from TurtleArt.tablock import Media +from TurtleArt.taconstants import CONSTANTS +from TurtleArt.tatype import * +from TurtleArt.tawindow import TurtleArtWindow + + +# search sys.path for a dir containing TurtleArt/tawindow.py +# path to the toplevel directory of the TA installation +_TA_INSTALLATION_PATH = None +for path in sys.path: + try: + entries = os.listdir(path) + except OSError: + continue + if "TurtleArt" in entries: + new_path = os.path.join(path, "TurtleArt") + try: + new_entries = os.listdir(new_path) + except OSError: + continue + if "tawindow.py" in new_entries: + _TA_INSTALLATION_PATH = path + break +# if the TA installation path was not found, notify the user and refuse to run +if _TA_INSTALLATION_PATH is None: + print _("The path to the TurtleArt installation must be listed in the " + "environment variable PYTHONPATH.") + exit(1) + +_PLUGIN_SUBPATH = 'plugins' +_MACROS_SUBPATH = 'macros' + + + +class DummyTurtleMain(object): + """Keep the main objects for running a dummy TA window in one place. + (Try not to have to inherit from turtleblocks.TurtleMain.) + """ + + def __init__(self, win, name="exported project"): + """Create a scrolled window to contain the turtle canvas. + win -- a GTK toplevel window + """ + self.win = win + self.set_title = self.win.set_title + + # setup a scrolled container for the canvas + self.vbox = gtk.VBox(False, 0) + self.vbox.show() + self.sw = gtk.ScrolledWindow() + self.sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + self.sw.show() + self.canvas = gtk.DrawingArea() + width = gtk.gdk.screen_width() * 2 + height = gtk.gdk.screen_height() * 2 + self.canvas.set_size_request(width, height) + self.sw.add_with_viewport(self.canvas) + self.canvas.show() + self.vbox.pack_end(self.sw, True, True) + self.win.add(self.vbox) + self.win.show_all() + + # exported code is always in interactive mode + interactive = True + + # copied from turtleblocks.TurtleMain._build_window() + if interactive: + gdk_win = self.canvas.get_window() + cr = gdk_win.cairo_create() + surface = cr.get_target() + else: + img_surface = cairo.ImageSurface(cairo.FORMAT_RGB24, + 1024, 768) + cr = cairo.Context(img_surface) + surface = cr.get_target() + self.turtle_canvas = surface.create_similar( + cairo.CONTENT_COLOR, max(1024, gtk.gdk.screen_width() * 2), + max(768, gtk.gdk.screen_height() * 2)) + + + + # instantiate an instance of a dummy sub-class that supports only + # the stuff TurtleGraphics needs + # TODO don't hardcode running_sugar + self.tw = TurtleArtWindow(self.canvas, _TA_INSTALLATION_PATH, + turtle_canvas=self.turtle_canvas, + parent=self, running_sugar=False, + running_turtleart=False) + + self.name = name + + + def _quit_ta(self, widget=None, e=None): + """Quit all plugins and the main window. No need to prompt the user + to save their work, since they cannot change anything. + """ + for plugin in self.tw.turtleart_plugins: + if hasattr(plugin, 'quit'): + plugin.quit() + gtk.main_quit() + exit() + + +def get_tw(): + """ Create a GTK window and instantiate a DummyTurtleMain instance. Return + the TurtleArtWindow object that holds the turtles and the canvas. + """ + # copied from turtleblocks.TurtleMain._setup_gtk() + + win = gtk.Window(gtk.WINDOW_TOPLEVEL) + gui = DummyTurtleMain(win=win, name=sys.argv[0]) + # TODO re-enable this code (after giving gui the right attributes) + # win.set_default_size(gui.width, gui.height) + # win.move(gui.x, gui.y) + win.maximize() + win.set_title(str(gui.name)) + # if os.path.exists(os.path.join(gui._execdirname, gui._ICON_SUBPATH)): + # win.set_icon_from_file(os.path.join(gui._execdirname, + # gui._ICON_SUBPATH)) + win.show() + win.connect('delete_event', gui._quit_ta) + + return gui.tw + + diff --git a/util/ast_extensions.py b/util/ast_extensions.py new file mode 100644 index 0000000..d46bdb0 --- /dev/null +++ b/util/ast_extensions.py @@ -0,0 +1,69 @@ +#Copyright (c) 2013 Marion Zepf + +#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. + + +""" Extend the `ast` module to include comments """ + +import ast + + +class ExtraCode(ast.stmt): + """Adds extra content to a primitive needed in Python code, e.g., + changes to the turtle (e.g., prim_turtle) require the addition of + turtle = turtles.get_active_turtle() + Extends the Python abstract grammar by the following: stmt + = ExtraContent(string text) | ... """ + + _fields = ('text') + + def __init__(self, text="", lineno=1, col_offset=0): + """ text -- the textual content of the comment, i.e. everything + directly following the hashtag until the next newline """ + self.text = text + self.lineno = lineno + self.col_offset = col_offset + + +class Comment(ast.stmt): + """ An inline comment, starting with a hashtag (#). + Extends the Python abstract grammar by the following: + stmt = Comment(string text) | ... """ + + _fields = ('text') + + def __init__(self, text="", lineno=1, col_offset=0): + """ text -- the textual content of the comment, i.e. everything + directly following the hashtag until the next newline """ + self.text = text + self.lineno = lineno + self.col_offset = col_offset + + +class LambdaWithStrBody(ast.Lambda): + """ Lambda AST whose body is a simple string (not ast.Str). + Extends the Python abstract grammar by the following: + expr = LambdaWithStrBody(string body_str, expr* args) | ... """ + + def __init__(self, body_str="", args=[], lineno=1, col_offset=0): + self.body_str = body_str + self.args = args + self.lineno = lineno + self.col_offset = col_offset + diff --git a/util/codegen.py b/util/codegen.py new file mode 100644 index 0000000..babada1 --- /dev/null +++ b/util/codegen.py @@ -0,0 +1,602 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2008, Armin Ronacher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +""" + codegen + ~~~~~~~ + + Extension to ast that allow ast -> python code generation. + + :copyright: Copyright 2008 by Armin Ronacher. + :license: BSD. + + Modified by Marion Zepf. +""" +from ast import * +from ast_extensions import Comment, ExtraCode + + +def to_source(node, indent_with=' ' * 4, add_line_information=False): + """This function can convert a node tree back into python sourcecode. + This is useful for debugging purposes, especially if you're dealing with + custom asts not generated by python itself. + + It could be that the sourcecode is evaluable when the AST itself is not + compilable / evaluable. The reason for this is that the AST contains some + more data than regular sourcecode does, which is dropped during + conversion. + + Each level of indentation is replaced with `indent_with`. Per default this + parameter is equal to four spaces as suggested by PEP 8, but it might be + adjusted to match the application's styleguide. + + If `add_line_information` is set to `True` comments for the line numbers + of the nodes are added to the output. This can be used to spot wrong line + number information of statement nodes. + """ + generator = SourceGenerator(indent_with, add_line_information) + generator.visit(node) + return ''.join(generator.result) + + +class SourceGenerator(NodeVisitor): + """This visitor is able to transform a well formed syntax tree into python + sourcecode. For more details have a look at the docstring of the + `node_to_source` function. + """ + + UNARYOP_SYMBOLS = {Invert: "~", Not: "not", UAdd: "+", USub: "-"} + # TODO use parentheses around expressions only where necessary + BINOP_SYMBOLS = {Add: "+", Sub: "-", Mult: "*", Div: "/", Mod: "%", + LShift: "<<", RShift:">>", BitOr: "|", BitXor: "^", + BitAnd: "&", FloorDiv: "//", Pow: "**"} + BOOLOP_SYMBOLS = {And: "and", Or: "or"} + CMPOP_SYMBOLS = {Eq: "==", NotEq: "!=", Lt: "<", LtE: "<=", Gt: ">", + GtE: ">=", Is: "is", IsNot: "is not", In: "in", + NotIn: "not in"} + + def __init__(self, indent_with, add_line_information=False): + self.result = [] + self.indent_with = indent_with + self.add_line_information = add_line_information + self.indentation = 0 + self.new_lines = 0 + + def write(self, x): + if self.new_lines: + if self.result: + self.result.append('\n' * self.new_lines) + self.result.append(self.indent_with * self.indentation) + self.new_lines = 0 + self.result.append(x) + + def newline(self, node=None, extra=0): + self.new_lines = max(self.new_lines, 1 + extra) + if node is not None and self.add_line_information: + self.write('# line: %s' % node.lineno) + self.new_lines = 1 + + def body(self, statements, do_indent=True): + if do_indent: + self.indentation += 1 + for stmt in statements: + self.newline() + self.visit(stmt) + if do_indent: + self.indentation -= 1 + + def body_or_else(self, node): + self.body(node.body) + if node.orelse: + self.newline() + self.write('else:') + self.body(node.orelse) + + def signature(self, node): + want_comma = [] + def write_comma(): + if want_comma: + self.write(', ') + else: + want_comma.append(True) + + padding = [None] * (len(node.args) - len(node.defaults)) + for arg, default in zip(node.args, padding + node.defaults): + write_comma() + self.visit(arg) + if default is not None: + self.write('=') + self.visit(default) + if node.vararg is not None: + write_comma() + self.write('*' + node.vararg) + if node.kwarg is not None: + write_comma() + self.write('**' + node.kwarg) + + def decorators(self, node): + for decorator in node.decorator_list: + self.newline(decorator) + self.write('@') + self.visit(decorator) + + # Statements + + def visit_Assign(self, node): + self.newline(node) + for idx, target in enumerate(node.targets): + if idx: + self.write(', ') + self.visit(target) + self.write(' = ') + self.visit(node.value) + + def visit_AugAssign(self, node): + self.newline(node) + self.visit(node.target) + self.write(self.BINOP_SYMBOLS[node.op] + '=') + self.visit(node.value) + + def visit_ImportFrom(self, node): + self.newline(node) + self.write('from %s%s import ' % ('.' * node.level, node.module)) + for idx, item in enumerate(node.names): + if idx: + self.write(', ') + self.visit(item) + + def visit_Import(self, node): + self.newline(node) + for item in node.names: + self.write('import ') + self.visit(item) + + def visit_Expr(self, node): + self.newline(node) + self.generic_visit(node) + + def visit_Module(self, node): + self.body(node.body, do_indent=False) + + def visit_FunctionDef(self, node): + self.newline(extra=1) + self.decorators(node) + self.newline(node) + self.write('def %s(' % node.name) + self.signature(node.args) + self.write('):') + self.body(node.body) + + def visit_ClassDef(self, node): + have_args = [] + def paren_or_comma(): + if have_args: + self.write(', ') + else: + have_args.append(True) + self.write('(') + + self.newline(extra=2) + self.decorators(node) + self.newline(node) + self.write('class %s' % node.name) + for base in node.bases: + paren_or_comma() + self.visit(base) + # XXX: the if here is used to keep this module compatible + # with python 2.6. + if hasattr(node, 'keywords'): + for keyword in node.keywords: + paren_or_comma() + self.write(keyword.arg + '=') + self.visit(keyword.value) + if node.starargs is not None: + paren_or_comma() + self.write('*') + self.visit(node.starargs) + if node.kwargs is not None: + paren_or_comma() + self.write('**') + self.visit(node.kwargs) + self.write(have_args and '):' or ':') + self.body(node.body) + + def visit_If(self, node): + self.newline(node) + self.write('if ') + self.visit(node.test) + self.write(':') + self.body(node.body) + while True: + else_ = node.orelse + if len(else_) == 1 and isinstance(else_[0], If): + node = else_[0] + self.newline() + self.write('elif ') + self.visit(node.test) + self.write(':') + self.body(node.body) + elif else_: + self.newline() + self.write('else:') + self.body(else_) + break + else: + break + + def visit_For(self, node): + self.newline(node) + self.write('for ') + self.visit(node.target) + self.write(' in ') + self.visit(node.iter) + self.write(':') + self.body_or_else(node) + + def visit_While(self, node): + self.newline(node) + self.write('while ') + self.visit(node.test) + self.write(':') + self.body_or_else(node) + + def visit_With(self, node): + self.newline(node) + self.write('with ') + self.visit(node.context_expr) + if node.optional_vars is not None: + self.write(' as ') + self.visit(node.optional_vars) + self.write(':') + self.body(node.body) + + def visit_Pass(self, node): + self.newline(node) + self.write('pass') + + def visit_Print(self, node): + # XXX: python 2.6 only + self.newline(node) + self.write('print ') + want_comma = False + if node.dest is not None: + self.write(' >> ') + self.visit(node.dest) + want_comma = True + for value in node.values: + if want_comma: + self.write(', ') + self.visit(value) + want_comma = True + if not node.nl: + self.write(',') + + def visit_Delete(self, node): + self.newline(node) + self.write('del ') + for idx, target in enumerate(node): + if idx: + self.write(', ') + self.visit(target) + + def visit_TryExcept(self, node): + self.newline(node) + self.write('try:') + self.body(node.body) + for handler in node.handlers: + self.visit(handler) + + def visit_TryFinally(self, node): + self.newline(node) + self.write('try:') + self.body(node.body) + self.newline(node) + self.write('finally:') + self.body(node.finalbody) + + def visit_Global(self, node): + self.newline(node) + self.write('global ' + ', '.join(node.names)) + + def visit_Nonlocal(self, node): + self.newline(node) + self.write('nonlocal ' + ', '.join(node.names)) + + def visit_Return(self, node): + self.newline(node) + self.write('return') + if hasattr(node, "value") and node.value is not None: + self.write(' ') + self.visit(node.value) + + def visit_Break(self, node): + self.newline(node) + self.write('break') + + def visit_Continue(self, node): + self.newline(node) + self.write('continue') + + def visit_Raise(self, node): + # XXX: Python 2.6 / 3.0 compatibility + self.newline(node) + self.write('raise') + if hasattr(node, 'exc') and node.exc is not None: + self.write(' ') + self.visit(node.exc) + if node.cause is not None: + self.write(' from ') + self.visit(node.cause) + elif hasattr(node, 'type') and node.type is not None: + self.visit(node.type) + if node.inst is not None: + self.write(', ') + self.visit(node.inst) + if node.tback is not None: + self.write(', ') + self.visit(node.tback) + + def visit_Comment(self, node): + self.newline(node) + self.write('#' + str(node.text)) + + def visit_ExtraCode(self, node): + self.newline(node) + self.write(str(node.text)) + + # Expressions + + def visit_Attribute(self, node): + self.visit(node.value) + self.write('.' + node.attr) + + def visit_Call(self, node): + want_comma = [] + def write_comma(): + if want_comma: + self.write(', ') + else: + want_comma.append(True) + + self.visit(node.func) + self.write('(') + for arg in node.args: + write_comma() + self.visit(arg) + for keyword in node.keywords: + write_comma() + self.write(keyword.arg + '=') + self.visit(keyword.value) + if node.starargs is not None: + write_comma() + self.write('*') + self.visit(node.starargs) + if node.kwargs is not None: + write_comma() + self.write('**') + self.visit(node.kwargs) + self.write(')') + visit_TypedCall = visit_Call + + def visit_Name(self, node): + self.write(node.id) + visit_TypedName = visit_Name + + def visit_Str(self, node): + self.write(repr(node.s)) + + def visit_Bytes(self, node): + self.write(repr(node.s)) + + def visit_Num(self, node): + self.write(repr(node.n)) + + def visit_Tuple(self, node): + self.write('(') + idx = -1 + for idx, item in enumerate(node.elts): + if idx: + self.write(', ') + self.visit(item) + self.write(idx and ')' or ',)') + + def sequence_visit(left, right): + def visit(self, node): + self.write(left) + for idx, item in enumerate(node.elts): + if idx: + self.write(', ') + self.visit(item) + self.write(right) + return visit + + visit_List = sequence_visit('[', ']') + visit_Set = sequence_visit('{', '}') + del sequence_visit + + def visit_Dict(self, node): + self.write('{') + for idx, (key, value) in enumerate(zip(node.keys, node.values)): + if idx: + self.write(', ') + self.visit(key) + self.write(': ') + self.visit(value) + self.write('}') + + def visit_BinOp(self, node): + op = self.BINOP_SYMBOLS[node.op] + # if op in ['+', '-']: + self.write('(') + self.visit(node.left) + self.write(' %s ' % op) + self.visit(node.right) + # if op in ['+', '-']: + self.write(')') + + def visit_BoolOp(self, node): + self.write('(') + for idx, value in enumerate(node.values): + if idx: + self.write(' %s ' % self.BOOLOP_SYMBOLS[node.op]) + self.visit(value) + self.write(')') + + def visit_Compare(self, node): + self.write('(') + self.visit(node.left) + for op, right in zip(node.ops, node.comparators): + self.write(' %s ' % self.CMPOP_SYMBOLS[op]) + self.visit(right) + self.write(')') + + def visit_UnaryOp(self, node): + self.write('(') + op = self.UNARYOP_SYMBOLS[node.op] + self.write(op) + if op == 'not': + self.write(' ') + self.visit(node.operand) + self.write(')') + + def visit_Subscript(self, node): + self.visit(node.value) + self.write('[') + self.visit(node.slice) + self.write(']') + visit_TypedSubscript = visit_Subscript + + def visit_Index(self, node): + self.visit(node.value) + + def visit_Slice(self, node): + if node.lower is not None: + self.visit(node.lower) + self.write(':') + if node.upper is not None: + self.visit(node.upper) + if node.step is not None: + self.write(':') + if not (isinstance(node.step, Name) and node.step.id == 'None'): + self.visit(node.step) + + def visit_ExtSlice(self, node): + for idx, item in node.dims: + if idx: + self.write(', ') + self.visit(item) + + def visit_Yield(self, node): + self.write('yield ') + self.visit(node.value) + + def visit_Lambda(self, node): + self.write('(lambda ') + self.signature(node.args) + self.write(': ') + self.visit(node.body) + self.write(')') + + def visit_LambdaWithStrBody(self, node): + self.write('(lambda ') + for idx, arg in enumerate(node.args): + if idx: + self.write(', ') + self.visit(arg) + self.write(': ') + self.write(node.body_str) + self.write(')') + + def visit_Ellipsis(self, node): + self.write('Ellipsis') + + def generator_visit(left, right): + def visit(self, node): + self.write(left) + self.visit(node.elt) + for comprehension in node.generators: + self.visit(comprehension) + self.write(right) + return visit + + visit_ListComp = generator_visit('[', ']') + visit_GeneratorExp = generator_visit('(', ')') + visit_SetComp = generator_visit('{', '}') + del generator_visit + + def visit_DictComp(self, node): + self.write('{') + self.visit(node.key) + self.write(': ') + self.visit(node.value) + for comprehension in node.generators: + self.visit(comprehension) + self.write('}') + + def visit_IfExp(self, node): + self.visit(node.body) + self.write(' if ') + self.visit(node.test) + self.write(' else ') + self.visit(node.orelse) + + def visit_Starred(self, node): + self.write('*') + self.visit(node.value) + + def visit_Repr(self, node): + # XXX: python 2.6 only + self.write('`') + self.visit(node.value) + self.write('`') + + # Helper Nodes + + def visit_alias(self, node): + self.write(node.name) + if node.asname is not None: + self.write(' as ' + node.asname) + + def visit_comprehension(self, node): + self.write(' for ') + self.visit(node.target) + self.write(' in ') + self.visit(node.iter) + if node.ifs: + for if_ in node.ifs: + self.write(' if ') + self.visit(if_) + + def visit_excepthandler(self, node): + self.newline(node) + self.write('except') + if node.type is not None: + self.write(' ') + self.visit(node.type) + if node.name is not None: + self.write(' as ') + self.visit(node.name) + self.write(':') + self.body(node.body) -- cgit v0.9.1