diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | __init__.py | 18 | ||||
-rw-r--r-- | activity/activity.info | 6 | ||||
-rw-r--r-- | activity/activity.info~ | 6 | ||||
-rw-r--r-- | activity/calculate.svg | 18 | ||||
-rw-r--r-- | activity/pyturtleicon.svg | 89 | ||||
-rw-r--r-- | activity/pyturtleicon.svg~ | 89 | ||||
-rw-r--r-- | console.py | 148 | ||||
-rw-r--r-- | customscrolledpanel.py | 42 | ||||
-rw-r--r-- | homedirectory.py | 35 | ||||
-rw-r--r-- | my_turtle.py | 52 | ||||
-rw-r--r-- | pyshell.py | 686 | ||||
-rw-r--r-- | pythonturtle.py | 174 | ||||
-rw-r--r-- | setup.py | 3 | ||||
-rw-r--r-- | smartsleep.py | 36 | ||||
-rw-r--r-- | turtleprocess.py | 254 | ||||
-rw-r--r-- | turtlewidget.py | 134 | ||||
-rw-r--r-- | vector.py | 143 |
18 files changed, 1934 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..f11b35a --- /dev/null +++ b/__init__.py @@ -0,0 +1,18 @@ +""" +An educational environment for learning Python, suitable for beginners +and children. Inspired by LOGO. + +Call `run()` to run it. + +PythonTurtle aims to be the most low-threshold way for learning or +teaching Python. The user is given command of an interactive Python +shell, similar to IDLE, and is able to use Python functions to move a +turtle that is displayed on the screen. An illustrated help screen +demonstrates how to move the turtle and introduces the student to the +basics of Python programming. +""" + +from pythonturtle import run + +if __name__=="__main__": + run()
\ No newline at end of file diff --git a/activity/activity.info b/activity/activity.info new file mode 100644 index 0000000..66246e4 --- /dev/null +++ b/activity/activity.info @@ -0,0 +1,6 @@ +[Activity] +name = PythonTurtle +bundle_id = org.laptop.PythonTurtle +icon = pyturtleicon +exec = sugar-activity pythonturtle.ApplicationWindow +activity_version = 1 diff --git a/activity/activity.info~ b/activity/activity.info~ new file mode 100644 index 0000000..66246e4 --- /dev/null +++ b/activity/activity.info~ @@ -0,0 +1,6 @@ +[Activity] +name = PythonTurtle +bundle_id = org.laptop.PythonTurtle +icon = pyturtleicon +exec = sugar-activity pythonturtle.ApplicationWindow +activity_version = 1 diff --git a/activity/calculate.svg b/activity/calculate.svg new file mode 100644 index 0000000..d338822 --- /dev/null +++ b/activity/calculate.svg @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ + <!ENTITY fill_color "#FFFFFF"> + <!ENTITY stroke_color "#000000"> +]> +<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50"> + <rect x="11" y="8" width="26" height="6" style="fill:&fill_color;;stroke:&stroke_color;;stroke-width:2"/> + <rect x="11" y="18" width="6" height="6" style="fill:&fill_color;;stroke:&stroke_color;;stroke-width:2"/> + <rect x="21" y="18" width="6" height="6" style="fill:&fill_color;;stroke:&stroke_color;;stroke-width:2"/> + <rect x="31" y="18" width="6" height="6" style="fill:&fill_color;;stroke:&stroke_color;;stroke-width:2"/> + <rect x="11" y="28" width="6" height="6" style="fill:&fill_color;;stroke:&stroke_color;;stroke-width:2"/> + <rect x="21" y="28" width="6" height="6" style="fill:&fill_color;;stroke:&stroke_color;;stroke-width:2"/> + <rect x="31" y="28" width="6" height="6" style="fill:&fill_color;;stroke:&stroke_color;;stroke-width:2"/> + <rect x="11" y="38" width="6" height="6" style="fill:&fill_color;;stroke:&stroke_color;;stroke-width:2"/> + <rect x="21" y="38" width="6" height="6" style="fill:&fill_color;;stroke:&stroke_color;;stroke-width:2"/> + <rect x="31" y="38" width="6" height="6" style="fill:&fill_color;;stroke:&stroke_color;;stroke-width:2"/> + +</svg> diff --git a/activity/pyturtleicon.svg b/activity/pyturtleicon.svg new file mode 100644 index 0000000..059521e --- /dev/null +++ b/activity/pyturtleicon.svg @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ + <!ENTITY fill_color "#FFFFFF"> + <!ENTITY stroke_color "#000000"> +]> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="48px" + height="48px" + id="svg2993" + version="1.1" + inkscape:version="0.48.3.1 r9886" + sodipodi:docname="New document 3"> + <defs + id="defs2995" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="7" + inkscape:cx="24" + inkscape:cy="24" + inkscape:current-layer="layer1" + showgrid="true" + inkscape:grid-bbox="true" + inkscape:document-units="px" + inkscape:window-width="851" + inkscape:window-height="671" + inkscape:window-x="0" + inkscape:window-y="27" + inkscape:window-maximized="0" /> + <metadata + id="metadata2998"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + id="layer1" + inkscape:label="Layer 1" + inkscape:groupmode="layer"> + <path + sodipodi:type="arc" + style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + id="path3771" + sodipodi:cx="18.214285" + sodipodi:cy="29" + sodipodi:rx="9.6428576" + sodipodi:ry="9.5714283" + d="M 27.857142,29 A 9.6428576,9.5714283 0 1 1 8.5714273,29 9.6428576,9.5714283 0 1 1 27.857142,29 z" /> + <rect + style="fill:none;stroke:#000000;stroke-width:1.11250508;stroke-opacity:1" + id="rect3773" + width="22.428572" + height="19.449062" + x="17.571428" + y="10.408082" /> + <path + sodipodi:type="arc" + style="fill:none;stroke:#000000;stroke-opacity:1" + id="path3775" + sodipodi:cx="22.428572" + sodipodi:cy="34.07143" + sodipodi:rx="9.5714283" + sodipodi:ry="9.6428576" + d="m 32,34.07143 a 9.5714283,9.6428576 0 1 1 -19.142857,0 9.5714283,9.6428576 0 1 1 19.142857,0 z" + transform="translate(2.7142856,2.2857142)" /> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 4.5714286,4.5714286 C 4,20.571429 4,21 4,21 l 0,0 10.428571,-10.714286 0.142858,0 z" + id="path3779" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/activity/pyturtleicon.svg~ b/activity/pyturtleicon.svg~ new file mode 100644 index 0000000..72072d4 --- /dev/null +++ b/activity/pyturtleicon.svg~ @@ -0,0 +1,89 @@ +<?xml version="1.0" ?> +<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' + 'http:www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd [ + <!ENTITY stroke_color "#000000"> + <!ENTITY fill_color "#FFFFFF"> +]><svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="48px" + height="48px" + id="svg2993" + version="1.1" + inkscape:version="0.48.3.1 r9886" + sodipodi:docname="New document 3"> + <defs + id="defs2995" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="7" + inkscape:cx="24" + inkscape:cy="24" + inkscape:current-layer="layer1" + showgrid="true" + inkscape:grid-bbox="true" + inkscape:document-units="px" + inkscape:window-width="851" + inkscape:window-height="671" + inkscape:window-x="0" + inkscape:window-y="27" + inkscape:window-maximized="0" /> + <metadata + id="metadata2998"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + id="layer1" + inkscape:label="Layer 1" + inkscape:groupmode="layer"> + <path + sodipodi:type="arc" + style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + id="path3771" + sodipodi:cx="18.214285" + sodipodi:cy="29" + sodipodi:rx="9.6428576" + sodipodi:ry="9.5714283" + d="M 27.857142,29 A 9.6428576,9.5714283 0 1 1 8.5714273,29 9.6428576,9.5714283 0 1 1 27.857142,29 z" /> + <rect + style="fill:none;stroke:#000000;stroke-width:1.11250508;stroke-opacity:1" + id="rect3773" + width="22.428572" + height="19.449062" + x="17.571428" + y="10.408082" /> + <path + sodipodi:type="arc" + style="fill:none;stroke:#000000;stroke-opacity:1" + id="path3775" + sodipodi:cx="22.428572" + sodipodi:cy="34.07143" + sodipodi:rx="9.5714283" + sodipodi:ry="9.6428576" + d="m 32,34.07143 a 9.5714283,9.6428576 0 1 1 -19.142857,0 9.5714283,9.6428576 0 1 1 19.142857,0 z" + transform="translate(2.7142856,2.2857142)" /> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 4.5714286,4.5714286 C 4,20.571429 4,21 4,21 l 0,0 10.428571,-10.714286 0.142858,0 z" + id="path3779" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/console.py b/console.py new file mode 100644 index 0000000..17b1162 --- /dev/null +++ b/console.py @@ -0,0 +1,148 @@ +""" +See documentation for the class Console defined here. +""" +import sys +import code +import traceback +import multiprocessing +from cStringIO import StringIO + + +class Console(code.InteractiveConsole): + """ + Console based on code.InteractiveConsole. You are supposed to run this + console in a process you create with the `multiprocessing` package. + It requires a parameter `queue_pack`, which you should create with + `make_queue_pack()` in this package. You are supposed to feed the same + queue pack into the Shell you will create for the two to be connected + to each other. + """ + def __init__(self,queue_pack,*args,**kwargs): + code.InteractiveConsole.__init__(self,*args,**kwargs) + + self.input_queue, self.output_queue, \ + self.runcode_finished_queue, self.runsource_return_queue = queue_pack + + self.readfunc=self.input_queue.get + self.writefunc=self.output_queue.put + + #self.stdin=PseudoFileIn(self.readfunc) + #self.stdout=PseudoFileOut(self.writefunc) + #self.stderr=PseudoFileErr(self.writefunc) + + + + def raw_input(self,prompt=None): + if prompt: self.write(prompt) + return self.readfunc() + + def write(self,output): + #self.log(output) + return self.writefunc(output) + + def log(self,output): + print(output); sys.stdout.flush() + + def push(self, command): + more = self.runsource(command, self.filename) + return more + + def showsyntaxerror(self, filename=None): + type, value, sys.last_traceback = sys.exc_info() + sys.last_type = type + sys.last_value = value + if filename and type is SyntaxError: + # Work hard to stuff the correct filename in the exception + try: + msg, (dummy_filename, lineno, offset, line) = value + except: + # Not the format we expect; leave it alone + pass + else: + # Stuff in the right filename + value = SyntaxError(msg, (filename, lineno, offset, line)) + sys.last_value = value + list = traceback.format_exception_only(type, value) + + map(self.write, list) + + def runsource(self, source, filename="<input>", symbol="single"): + try: + code = self.compile(source, filename, symbol) + except (OverflowError, SyntaxError, ValueError): + # Case 1 + self.showsyntaxerror(filename) + + self.runsource_return_queue.put(False) + self.runcode_finished_queue.put(None) + return False + + if code is None: + # Case 2 + + self.runsource_return_queue.put(True) + self.runcode_finished_queue.put(None) + return True + + # Case 3 + self.runsource_return_queue.put(False) + + #stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr + #sys.stdin, sys.stdout, sys.stderr = self.stdin, self.stdout, self.stderr + result = StringIO() + sys.stdout = result + + try: + self.runcode(code) + finally: + self.output_queue.put(result.getvalue()) + sys.stdout = sys.__stdout__ + # if sys.stdin==self.stdin: sys.stdin=stdin + # if sys.stdout==self.stdout: sys.stdout=stdout + # if sys.stderr==self.stderr: sys.stderr=stderr + + self.runcode_finished_queue.put(None) + return False + + def interact(self, banner=None): + try: + sys.ps1 + except AttributeError: + sys.ps1 = ">>> " + try: + sys.ps2 + except AttributeError: + sys.ps2 = "... " + cprt = 'Type "help", "copyright", "credits" or "license" for more information.' + """ + if banner is None: + self.write("Python %s on %s\n%s\n(%s)\n" % + (sys.version, sys.platform, cprt, + self.__class__.__name__)) + else: + self.write("%s\n" % str(banner)) + """ + more = 0 + while 1: + try: + + if more: + prompt = sys.ps2 + else: + prompt = sys.ps1 + try: + line = self.raw_input()#prompt) + # Can be None if sys.stdin was redefined + encoding = getattr(sys.stdin, "encoding", None) + if encoding and not isinstance(line, unicode): + line = line.decode(encoding) + except EOFError: + self.write("\n") + break + else: + #self.log(line.__repr__()) + more = self.push(line) + except KeyboardInterrupt: + self.write("\nKeyboardInterrupt\n") + self.resetbuffer() + more = 0 diff --git a/customscrolledpanel.py b/customscrolledpanel.py new file mode 100644 index 0000000..72951f6 --- /dev/null +++ b/customscrolledpanel.py @@ -0,0 +1,42 @@ +""" +See documentation for CustomScrolledPanel which is defined in this module. +""" + +import wx.lib.scrolledpanel + +class CustomScrolledPanel(wx.lib.scrolledpanel.ScrolledPanel): + """ + A subclass of wx.lib.scrolledpanel.ScrolledPanel, which implements + usage of the Home and the End keys. + """ + def __init__(self, *args, **kwargs): + wx.lib.scrolledpanel.ScrolledPanel.__init__(self, *args, **kwargs) + self.Bind(wx.EVT_KEY_DOWN, self.on_key_down) + + def on_key_down(self, event): + """ + Event handler for the key-down event. + """ + key = event.GetKeyCode() + if key in (wx.WXK_HOME, wx.WXK_NUMPAD_HOME): + self.scroll_home() + return + elif key in (wx.WXK_END, wx.WXK_NUMPAD_END): + self.scroll_end() + return + else: + event.Skip() + + def scroll_home(self): + """ + Scrolls the panel to its top. + """ + self.Scroll(-1, 0) + + def scroll_end(self): + """ + Scrolls the panel to its bottom. + """ + bottom = self.GetVirtualSize()[1] + self.Scroll(-1, bottom) + diff --git a/homedirectory.py b/homedirectory.py new file mode 100644 index 0000000..13d83ce --- /dev/null +++ b/homedirectory.py @@ -0,0 +1,35 @@ +""" +Module for finding out the directory of the program we are running, +be it a Python script or an executable. +Use homedirectory.do() to set this path as the current os path and to +add it to sys.path. +""" + +import os +import sys + + +def _are_we_frozen(): + """Returns whether we are frozen via py2exe. + This will affect how we find out where we are located.""" + + return hasattr(sys, "frozen") + + +def our_path(): + """ This will get us the program's directory, + even if we are frozen using py2exe""" + + if _are_we_frozen(): + return os.path.dirname(unicode(sys.executable, sys.getfilesystemencoding( ))) + + return os.path.dirname(unicode(__file__, sys.getfilesystemencoding( ))) + +def do(): + """ + Sets the directory containing our program as the current directory in os, + as well as adding it to sys.path. + """ + path=our_path() + os.chdir(path) + sys.path.append(path)
\ No newline at end of file diff --git a/my_turtle.py b/my_turtle.py new file mode 100644 index 0000000..db0868a --- /dev/null +++ b/my_turtle.py @@ -0,0 +1,52 @@ +from vector import Vector +from misc.angles import deg_to_rad, rad_to_deg + + +# Size of the turtle canvas. We assume no user will have a screen +# so big that the canvas will be bigger than this. +BITMAP_SIZE = Vector((2000,1200)) + +# Center of the canvas. +origin = BITMAP_SIZE / 2.0 + + +# 4 lambda functions for transforming between the reference frame +# that we prefer and the reference frame that wxPython prefers: + +to_my_angle = lambda angle: rad_to_deg(-angle)-180 +from_my_angle = lambda angle: deg_to_rad(-angle+180) + +from_my_pos = lambda pos: -pos+origin +to_my_pos = lambda pos: -pos+origin + +############## + +class Turtle(object): + """ + A Turtle object defines a turtle by its attributes, such as + position, orientation, color, etc. See source of __init__ for + a complete list. + """ + def __init__(self): + self.pos = Vector((0,0)) + self.orientation = 180 + self.color = "red" + self.rgb_color = (255, 0, 0) + self.width = 3 + self.visible = True + self.pen_down = True + + # the `clear` attribute is only made True momentarily when + # the `clear()` function is called by the user to clear the screen. + self.clear = False + + self.SPEED=400.0 # Pixels per second + self.ANGULAR_SPEED=360.0 # Degrees per second + + + def give_pen(self): + """ + Gives a wxPython pen that corresponds to the color, width, + and pen_downity of the Turtle instance. + """ + return wx.Pen(self.color,self.width,wx.SOLID if self.pen_down else wx.TRANSPARENT) diff --git a/pyshell.py b/pyshell.py new file mode 100644 index 0000000..d7c3243 --- /dev/null +++ b/pyshell.py @@ -0,0 +1,686 @@ +# +# Py_Shell.py : inserts the python prompt in a gtk interface +# + +import sys, code, os +import __builtin__ + +from gi.repository import GObject, Pango, Gdk, GObject +from gi.repository import Gtk as gtk +import Queue + +PS1=">>> " +PS2="... " +TAB_WIDTH=4 +SPACEBAR = 32 + +BANNER="Python "+sys.version+"\n" + + + +class Completer: + """ + Taken from rlcompleter, with readline references stripped, and a local dictionary to use. + """ + def __init__(self,locals): + self.locals = locals + + def complete(self, text, state): + """Return the next possible completion for 'text'. + This is called successively with state == 0, 1, 2, ... until it + returns None. The completion should begin with 'text'. + + """ + if state == 0: + if "." in text: + self.matches = self.attr_matches(text) + else: + self.matches = self.global_matches(text) + try: + return self.matches[state] + except IndexError: + return None + + def global_matches(self, text): + """Compute matches when text is a simple name. + + Return a list of all keywords, built-in functions and names + currently defines in __main__ that match. + + """ + import keyword + matches = [] + n = len(text) + for list in [keyword.kwlist, __builtin__.__dict__.keys(), self.locals.keys()]: + for word in list: + if word[:n] == text and word != "__builtins__": + matches.append(word) + return matches + + def attr_matches(self, text): + """Compute matches when text contains a dot. + + Assuming the text is of the form NAME.NAME....[NAME], and is + evaluatable in the globals of __main__, it will be evaluated + and its attributes (as revealed by dir()) are used as possible + completions. (For class instances, class members are are also + considered.) + + WARNING: this can still invoke arbitrary C code, if an object + with a __getattr__ hook is evaluated. + + """ + import re + m = re.match(r"(\w+(\.\w+)*)\.(\w*)", text) + if not m: + return + expr, attr = m.group(1, 3) + object = eval(expr, self.locals, self.locals) + words = dir(object) + if hasattr(object,'__class__'): + words.append('__class__') + words = words + get_class_members(object.__class__) + matches = [] + n = len(attr) + for word in words: + if word[:n] == attr and word != "__builtins__": + matches.append("%s.%s" % (expr, word)) + return matches + +def get_class_members(klass): + ret = dir(klass) + if hasattr(klass,'__bases__'): + for base in klass.__bases__: + ret = ret + get_class_members(base) + return ret + + + + + + + +class Dummy_File: + + def __init__(self, buffer, tag): + """Implements a file-like object for redirect the stream to the buffer""" + + self.buffer = buffer + self.tag = tag + + def write(self, text): + """Write text into the buffer and apply self.tag""" + iter=self.buffer.get_end_iter() + self.buffer.insert_with_tags(iter,text,self.tag) + + def writelines(self, l): + map(self.write, l) + + def flush(self): + pass + + def isatty(self): + return 1 + + + +class PopUp: + + def __init__(self, text_view, list, position): + self.text_view=text_view + + #avoid duplicate items in list + tmp={} + n_chars=0 + for item in list: + dim=len(item) + if dim>n_chars: + n_chars=dim + tmp[item]=None + list=tmp.keys() + list.sort() + + self.list=list + self.position=position + self.popup=gtk.Window(gtk.WINDOW_POPUP) + frame=gtk.Frame() + sw=gtk.ScrolledWindow() + sw.set_policy(gtk.GTK_POLICY_AUTOMATIC, gtk.GTK_POLICY_AUTOMATIC) + model=gtk.ListStore(GObject.TYPE_STRING) + for item in self.list: + iter=model.append() + model.set(iter, 0, item) + self.list_view=gtk.TreeView(model) + self.list_view.connect("row-activated", self.hide) + self.list_view.set_property("headers-visible", False) + selection=self.list_view.get_selection() + selection.connect("changed",self.select_row) + selection.select_path((0,)) + renderer=gtk.CellRendererText() + column=gtk.TreeViewColumn("",renderer,text=0) + self.list_view.append_column(column) + sw.add(self.list_view) + frame.add(sw) + self.popup.add(frame) + + #set the width of the popup according with the length of the strings + contest=self.popup.get_pango_context() + desc=contest.get_font_description() + lang=contest.get_language() + metrics= contest.get_metrics(desc, lang) + width= Pango.PIXELS(metrics.get_approximate_char_width()* n_chars) + if width>80: + self.popup.set_size_request(width,90) + else: + self.popup.set_size_request(80,90) + self.show_popup() + + + def hide(self, *arg): + self.popup.hide() + + def show_popup(self): + buffer=self.text_view.get_buffer() + iter=buffer.get_iter_at_mark(buffer.get_insert()) + + rectangle=self.text_view.get_iter_location(iter) + absX, absY=self.text_view.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT, + rectangle.x+rectangle.width+20 , + rectangle.y+rectangle.height+50) + parent=self.text_view.get_parent() + self.popup.move(self.position[0]+absX, self.position[1]+absY) + self.popup.show_all() + + + + def prev(self): + sel=self.list_view.get_selection() + model, iter=sel.get_selected() + newIter=model.get_path(iter) + if newIter!=None and newIter[0]>0: + path=(newIter[0]-1,) + self.list_view.set_cursor(path) + + + def next(self): + sel=self.list_view.get_selection() + model, iter=sel.get_selected() + newIter=model.iter_next(iter) + if newIter!=None: + path=model.get_path(newIter) + self.list_view.set_cursor(path) + + + def sel_confirmed(self): + sel=self.list_view.get_selection() + self.select_row(sel) + self.hide() + + + def select_row(self, selection): + model, iter= selection.get_selected() + name=model.get_value(iter,0) + buffer=self.text_view.get_buffer() + end=buffer.get_iter_at_mark(buffer.get_insert()) + start=end.copy() + start.backward_char() + while start.get_char() not in " ,()[]": + start.backward_char() + start.forward_char() + buffer.delete(start,end) + iter=buffer.get_iter_at_mark(buffer.get_insert()) + buffer.insert(iter,name) + + +class Shell_Gui: + + def __init__(self,with_window=1,banner=BANNER, label_text="Interactive Python Shell", namespace=None, queue_pack=None): + + self.queue_pack = queue_pack + self.code_lines = [] + self.banner=banner + box=gtk.HBox() + box.set_homogeneous(False) + box.set_border_width(4) + box.set_spacing(4) + sw=gtk.ScrolledWindow() + + self.view=gtk.TextView() + self.buffer = self.view.get_buffer() + # creates three tags + tag_err=self.buffer.create_tag("error") + tag_err.set_property("foreground","red") + tag_err.set_property("font","monospace 10") + + tag_out=self.buffer.create_tag("out_tag") + tag_out.set_property("foreground","blue") + tag_out.set_property("font","monospace 10") + + tag_in=self.buffer.create_tag("in_tag") + tag_in.set_property("foreground","black") + tag_in.set_property("font","monospace 10") + + tag_no_edit=self.buffer.create_tag("no_edit") + tag_no_edit.set_property("editable",False) + #add the banner + self.buffer.set_text(self.banner+PS1) + start,end=self.buffer.get_bounds() + self.buffer.apply_tag_by_name("out_tag", start, end) + self.buffer.apply_tag_by_name("no_edit", start, end) + + self.view.connect("key_press_event", self.key_press) + self.view.connect("drag_data_received",self.drag_data_received) + + GObject.timeout_add(30, self.on_idle) + + self.view.set_wrap_mode(gtk.WrapMode.WORD_CHAR) + sw.add(self.view) + box.pack_start(sw, True, True, 0) + + #creates two dummy files + self.dummy_out=Dummy_File(self.buffer,tag_out) + self.dummy_err=Dummy_File(self.buffer,tag_err) + + #creates the console + if namespace is None: namespace = {} + self.core=code.InteractiveConsole(namespace) + + #autocompletation capabilities + self.completer = Completer(self.core.locals) + self.popup=None + + #creates history capabilities + self.history=[" "] + self.history_pos=0 + + #add buttons + #b_box=gtk.Toolbar() + #b_box.set_orientation(gtk.Orientation.VERTICAL) + #b_box.set_style(gtk.ToolbarStyle.ICONS) + + #button1 = gtk.ToolButton(gtk.STOCK_SAVE) + #button2 = gtk.ToolButton(gtk.STOCK_CLEAR) + #button3 = gtk.ToolButton(gtk.STOCK_PREFERENCES) + + #b_box.insert(gtk.STOCK_CLEAR,"Clear the output", None, self.clear_text, None,-1) + #b_box.insert(gtk.STOCK_SAVE,"Save the output", None, self.save_text, None,-1) + #b_box.insert(gtk.STOCK_PREFERENCES,"Preferences", None, None, None,-1) + #b_box.insert(button1, 0) + #b_box.insert(button2, 0) + #b_box.insert(button3, 0) + + if with_window: + q_button = gtk.ToolButton(gtk.STOCK_QUIT) + #b_box.insert(q_button, 0) + + + #box.pack_start(b_box, False, True, 0) + frame=gtk.Frame() + frame.set_label(label_text) + frame.show_all() + frame.add(box) + + + if with_window: + self.gui=gtk.Window() + self.gui.add(frame) + self.gui.connect("delete-event",self.quit) + self.gui.set_default_size(520,200) + self.gui.show_all() + else: + self.gui=frame + + + + + + def key_press(self, view, event): + if self.popup!=None: + + if event.keyval == Gdk.KEY_Up: + self.popup.prev() + return True + elif event.keyval == Gdk.KEY_Down: + self.popup.next() + return True + elif event.keyval == Gdk.KEY_Return: + self.popup.sel_confirmed() + self.popup=None + return True + else: + self.popup.hide() + self.popup=None + else: + if event.keyval == Gdk.KEY_Up: + + if self.history_pos>0: + # remove text into the line... + end=self.buffer.get_end_iter() + start=self.buffer.get_iter_at_line(end.get_line()) + start.forward_chars(4) + self.buffer.delete(start,end) + #inset the new text + pos=self.buffer.get_end_iter() + self.buffer.insert(pos, self.history[self.history_pos]) + self.history_pos-=1 + else: + Gdk.beep() + self.view.emit_stop_by_name("key-press-event") + return True + + elif event.keyval == Gdk.KEY_Down: + + if self.history_pos<len(self.history)-1: + # remove text into the line... + end=self.buffer.get_end_iter() + start=self.buffer.get_iter_at_line(end.get_line()) + start.forward_chars(4) + self.buffer.delete(start,end) + #inset the new text + pos=self.buffer.get_end_iter() + self.history_pos+=1 + self.buffer.insert(pos, self.history[self.history_pos]) + + else: + Gdk.beep() + self.view.emit_stop_by_name("key-press-event") + return True + + elif event.keyval == Gdk.KEY_Tab: + iter=self.buffer.get_iter_at_mark(self.buffer.get_insert()) + self.buffer.insert(iter,TAB_WIDTH*" ") + return True + + elif event.keyval == Gdk.KEY_Return: + command=self.get_line() + self.exec_code(command) + start,end=self.buffer.get_bounds() + self.buffer.apply_tag_by_name("no_edit",start,end) + self.buffer.place_cursor(end) + return True + + #elif event.keyval == SPACEBAR: #and event.state & gtk.gdk.CONTROL_MASK: + # self.complete_text() + # return True + + + + def clear_text(self,*widget): + dlg=gtk.Dialog("Clear") + dlg.add_button("Clear",1) + dlg.add_button("Reset",2) + dlg.add_button(gtk.STOCK_CLOSE,gtk.RESPONSE_CLOSE) + dlg.set_default_size(250,150) + hbox=gtk.HBox() + #add an image + img=gtk.Image() + img.set_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_DIALOG) + hbox.pack_start(img, True, True, 0) + + #add text + text="You have two options:\n" + text+=" -clear only the output window\n" + text+=" -reset the shell\n" + text+="\n What do you want to do?" + label=gtk.Label(text) + hbox.pack_start(label, True, True, 0) + + hbox.show_all() + dlg.vbox.pack_start(hbox, True, True, 0) + + ans=dlg.run() + dlg.hide() + if ans==1: + self.buffer.set_text(self.banner+PS1) + start,end=self.buffer.get_bounds() + self.buffer.apply_tag_by_name("out_tag",start,end) + + elif ans==2: + self.buffer.set_text(self.banner+PS1) + start,end=self.buffer.get_bounds() + self.buffer.apply_tag_by_name("out_tag",start,end) + self.buffer.apply_tag_by_name("no_edit",start,end) + + #creates the console + self.core=code.InteractiveConsole() + #reset history + self.history=[" "] + self.history_pos=0 + self.view.grab_focus() + + + def save_text(self, *widget): + dlg=gtk.Dialog("Save to file") + dlg.add_button("Commands",1) + dlg.add_button("All",2) + dlg.add_button(gtk.STOCK_CLOSE,gtk.RESPONSE_CLOSE) + dlg.set_default_size(250,150) + hbox=gtk.HBox() + #add an image + img=gtk.Image() + img.set_from_stock(gtk.STOCK_SAVE, gtk.ICON_SIZE_DIALOG) + hbox.pack_start(img) + + #add text + text="You have two options:\n" + text+=" -save only commands\n" + text+=" -save all\n" + text+="\n What do you want to save?" + label=gtk.Label(text) + hbox.pack_start(label) + + hbox.show_all() + dlg.vbox.pack_start(hbox) + + ans=dlg.run() + dlg.hide() + if ans==1 : + def ok_save(button, data=None): + win =button.get_toplevel() + win.hide() + name=win.get_filename() + if os.path.isfile(name): + box=gtk.MessageDialog(dlg, + gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_QUESTION,gtk.BUTTONS_YES_NO, + name+" already exists; do you want to overwrite it?" + ) + ans=box.run() + box.hide() + if ans==gtk.RESPONSE_NO: + return + try: + file=open(name,'w') + for i in self.history: + file.write(i) + file.write("\n") + file.close() + + + except Exception, x: + box=gtk.MessageDialog(dlg, + gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_ERROR,gtk.BUTTONS_CLOSE, + "Unable to write \n"+ + name+"\n on disk \n\n%s"%(x) + ) + box.run() + box.hide() + + def cancel_button(button): + win.get_toplevel() + win.hide() + + win=gtk.FileSelection("Save Commands...") + win.ok_button.connect_object("clicked", ok_save,win.ok_button) + win.cancel_button.connect_object("clicked", cancel_button,win.cancel_button) + win.show() + elif ans==2: + def ok_save(button, data=None): + win =button.get_toplevel() + win.hide() + name=win.get_filename() + if os.path.isfile(name): + box=gtk.MessageDialog(dlg, + gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_QUESTION,gtk.BUTTONS_YES_NO, + name+" already exists; do you want to overwrite it?" + ) + ans=box.run() + box.hide() + if ans==gtk.RESPONSE_NO: + return + try: + start,end=self.buffer.get_bounds() + text=self.buffer.get_text(start,end,0) + file=open(name,'w') + file.write(text) + file.close() + + except Exception, x: + box=gtk.MessageDialog(dlg, + gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_ERROR,gtk.BUTTONS_CLOSE, + "Unable to write \n"+ + name+"\n on disk \n\n%s"%(x) + ) + box.run() + box.hide() + + def cancel_button(button): + win.get_toplevel() + win.hide() + + win=gtk.FileSelection("Save Log...") + win.ok_button.connect_object("clicked", ok_save,win.ok_button) + win.cancel_button.connect_object("clicked", cancel_button,win.cancel_button) + win.show() + dlg.destroy() + self.view.grab_focus() + + + + def get_line(self): + iter=self.buffer.get_iter_at_mark(self.buffer.get_insert()) + line=iter.get_line() + start=self.buffer.get_iter_at_line(line) + end=start.copy() + end.forward_line() + command=self.buffer.get_text(start,end,0) + if (command[:4]==PS1 or command[:4]==PS2): + command=command[4:] + return command + + + def complete_text(self): + end=self.buffer.get_iter_at_mark(self.buffer.get_insert()) + start=end.copy() + start.backward_char() + while start.get_char() not in " ,()[]=": + start.backward_char() + start.forward_char() + token=self.buffer.get_text(start,end,0).strip() + completions = [] + try: + p=self.completer.complete(token,len(completions)) + while p != None: + completions.append(p) + p=self.completer.complete(token, len(completions)) + except: + return + if len(completions)==1: + self.buffer.delete(start,end) + iter=self.buffer.get_iter_at_mark(self.buffer.get_insert()) + self.buffer.insert(iter,completions[0]) + elif len(completions)>1: + #show a popup + if isinstance(self.gui, gtk.Frame): + rect=self.gui.get_allocation() + app=self.gui.window.get_position() + position=(app[0]+rect.x,app[1]+rect.y) + else: + position=self.gui.get_position() + + self.popup=PopUp(self.view, completions, position) + + + def replace_line(self, text): + iter=self.buffer.get_iter_at_mark(self.buffer.get_insert()) + line=iter.get_line() + start=self.buffer.get_iter_at_line(line) + start.forward_chars(4) + end=start.copy() + end.forward_line() + self.buffer.delete(start,end) + iter=self.buffer.get_iter_at_mark(self.buffer.get_insert()) + self.buffer.insert(iter, text) + + + + def sdt2files(self): + """switch stdin stdout stderr to my dummy files""" + self.std_out_saved=sys.stdout + self.std_err_saved=sys.stderr + + sys.stdout=self.dummy_out + sys.stderr=self.dummy_err + + + + + + + def files2sdt(self): + """switch my dummy files to stdin stdout stderr """ + sys.stdout=self.std_out_saved + sys.stderr=self.std_err_saved + + + + + def drag_data_received(self, source, drag_context, n1, n2, selection_data, long1, long2): + print selection_data.data + + def on_idle(self, e=None): + try: + result = self.queue_pack[1].get(False) + self.dummy_out.write(result) + self.dummy_out.write("%s" % (PS1)) + return True + except Queue.Empty: + pass + return True + + def exec_code(self, text): + """Execute text into the console and display the output into TextView""" + + #update history + self.history.append(text) + self.history_pos=len(self.history)-1 + + self.dummy_out.write("\n") + self.code_lines.append(text) + cur_code = "\n".join(self.code_lines) + action = self.core.compile(cur_code) + if action: + # the line needs to be executed + self.code_lines = [] + self.queue_pack[0].put(cur_code) + else: + self.dummy_out.write(PS2) + + self.view.scroll_mark_onscreen(self.buffer.get_insert()) + + + + def quit(self,*args): + if __name__=='__main__': + gtk.main_quit() + else: + if self.popup!=None: + self.popup.hide() + self.gui.hide() + +if __name__ == "__main__": + Shell_Gui() + gtk.main() diff --git a/pythonturtle.py b/pythonturtle.py new file mode 100644 index 0000000..454840c --- /dev/null +++ b/pythonturtle.py @@ -0,0 +1,174 @@ +""" +Main module which defines ApplicationWindow, +the main window of PythonTurtle. +""" +# Now porting from wxPython to Gtk3 to use in Sugar +#from customscrolledpanel import CustomScrolledPanel +#import shelltoprocess +import sys +import turtlewidget +import turtleprocess +import multiprocessing +import homedirectory; homedirectory.do() +from misc.fromresourcefolder import from_resource_folder + +from gi.repository import Gtk +import pyshell + +from sugar3.activity import activity +from sugar3.graphics.toolbarbox import ToolbarBox +from sugar3.activity.widgets import StopButton + +class ApplicationWindow(activity.Activity): + """ + The main window of PythonTurtle. + """ + def __init__(self, handle): + activity.Activity.__init__(self, handle) + + # create the toolbar + toolbar_box = ToolbarBox() + stop = StopButton(self) + toolbar_box.toolbar.insert(stop, -1) + stop.show() + self.set_toolbar_box(toolbar_box) + toolbar_box.show() + + self.turtle_process = turtleprocess.TurtleProcess() + self.turtle_process.start() + self.turtle_queue = self.turtle_process.turtle_queue + + #self.init_menu_bar() + + #self.init_about_dialog_info() + + #self.splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE) + self.box = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL) + self.add(self.box) + + self.turtle_widget = turtlewidget.TurtleWidget(self.turtle_queue) + self.box.add1(self.turtle_widget) + + self.shell = pyshell.Shell_Gui(with_window=0, queue_pack=self.turtle_process.queue_pack) + self.box.add2(self.shell.gui) + self.box.set_position(100) + #self.connect("destroy", Gtk.main_quit) + self.set_canvas(self.box) + self.box.show_all() + + #self.bottom_sizer_panel = wx.Panel(self.splitter) + + #self.shell = \ + # shelltoprocess.Shell(self.bottom_sizer_panel, + # queue_pack=self.turtle_process.queue_pack) + + + def init_help_screen(self): + """ + Initializes the help screen. + """ + self.help_screen = wx.Panel(parent=self, size=(-1,-1)) + + self.help_notebook = \ + wx.aui.AuiNotebook(parent=self.help_screen, style=wx.aui.AUI_NB_TOP) + + + def give_focus_to_selected_page(event=None): + selected_page_number = self.help_notebook.GetSelection() + selected_page = self.help_notebook.GetPage(selected_page_number) + if self.FindFocus() != selected_page: + selected_page.SetFocus() + + self.help_notebook.Bind(wx.EVT_SET_FOCUS, give_focus_to_selected_page) + self.help_notebook.Bind(wx.EVT_CHILD_FOCUS, give_focus_to_selected_page) + + self.help_images_list=[["Level 1", from_resource_folder("help1.png")], + ["Level 2", from_resource_folder("help2.png")], + ["Level 3", from_resource_folder("help3.png")], + ["Level 4", from_resource_folder("help4.png")]] + + + self.help_pages=[HelpPage(parent=self.help_notebook, bitmap=wx.Bitmap(bitmap_file), caption=caption) \ + for [caption, bitmap_file] in self.help_images_list] + + for page in self.help_pages: + self.help_notebook.AddPage(page, caption=page.caption) + + self.help_close_button_panel = wx.Panel(parent=self.help_screen) + self.help_screen_sizer = wx.BoxSizer(wx.VERTICAL) + self.help_screen_sizer.Add(self.help_notebook, 1, wx.EXPAND) + self.help_screen_sizer.Add(self.help_close_button_panel, 0, wx.EXPAND) + self.help_screen.SetSizer(self.help_screen_sizer) + + help_close_button_bitmap=wx.Bitmap(from_resource_folder("lets_code.png")) + self.help_close_button = \ + wx.lib.buttons.GenBitmapButton(self.help_close_button_panel, -1, + help_close_button_bitmap) + self.help_close_button_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.help_close_button_sizer.Add(self.help_close_button, 1, wx.EXPAND | wx.ALL, 5) + self.help_close_button_panel.SetSizer(self.help_close_button_sizer) + + self.Bind(wx.EVT_BUTTON, self.hide_help, self.help_close_button) + + + def show_help(self, event=None): + self.help_shown=True + self.help_menu_item.Check() + self.sizer.Show(self.help_screen) + self.sizer.Hide(self.splitter) + self.help_notebook.SetFocus() + self.sizer.Layout() + + def hide_help(self, event=None): + self.help_shown=False + self.help_menu_item.Check(False) + self.sizer.Hide(self.help_screen) + self.sizer.Show(self.splitter) + self.shell.setFocus() + self.sizer.Layout() + + def toggle_help(self, event=None): + if self.help_shown: + self.hide_help() + else: + self.show_help() + + def on_exit(self, event=None): + return self.Close() + + def init_about_dialog_info(self): + info = self.about_dialog_info = \ + wx.AboutDialogInfo() + + description="""\ +An educational environment for learning Python, suitable for beginners and children. +Inspired by LOGO. + +Runs on Python 2.6, using wxPython, Psyco and py2exe. Thanks go to the developers +responsible for these projects, as well as to the helpful folks at the user groups +of these projects, and at StackOverflow.com, who have helped solved many problems +that came up in the making of this program.""" + + info.SetCopyright("MIT License, (C) 2009 Ram Rachum (\"cool-RR\")") + info.SetDescription(description) + info.SetName("PythonTurtle") + info.SetVersion("0.1.2009.8.2.1") + info.SetWebSite("http://pythonturtle.com") + + + def on_about(self, event=None): + about_dialog = wx.AboutBox(self.about_dialog_info) + + +def run(): + multiprocessing.freeze_support() + app = wx.PySimpleApp() + my_app_win = ApplicationWindow(None,-1,"PythonTurtle",size=(600,600)) + #import cProfile; cProfile.run("app.MainLoop()") + app.MainLoop() + +#if __name__=="__main__": + #run() + #w = ApplicationWindow() + #w.resize(600, 600) + #Gtk.main() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..73889e5 --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +from sugar3.activity import bundlebuilder + +bundlebuilder.start() diff --git a/smartsleep.py b/smartsleep.py new file mode 100644 index 0000000..293c1d9 --- /dev/null +++ b/smartsleep.py @@ -0,0 +1,36 @@ +""" +A short module for implementing `Sleeper`. See its documentation. +""" + +import time +import sys + +#def log(text): print(text); sys.stdout.flush() + +class Sleeper(object): + """ + A smarter way to use `time.sleep()`, implemented as a context manager. + Use it like this: + + with smartsleep.Sleeper(7): + do_stuff() + + The Sleeper instance will ensure that at least 7 second have passed before + control flows to the next lines of code. + """ + def __init__(self,interval): + self.interval=interval + + def __enter__(self,*args,**kwargs): + self.starting_time=time.time() + + def __exit__(self,*args,**kwargs): + #global log + time_now=time.time() + interval_gone=time_now-self.starting_time + if interval_gone>=self.interval: + #log("didn't sleep") + return + else: + #log("slept") + time.sleep(self.interval-interval_gone)
\ No newline at end of file diff --git a/turtleprocess.py b/turtleprocess.py new file mode 100644 index 0000000..e4af0f8 --- /dev/null +++ b/turtleprocess.py @@ -0,0 +1,254 @@ +import sys +import multiprocessing +import copy +import math +import time + +import smartsleep +import misc.angles as angles +import pyshell +import console +from my_turtle import * +from vector import Vector + +def log(x): + print(x) + sys.stdout.flush() + +class TurtleProcess(multiprocessing.Process): + """ + A TurtleProcess is a subclass of multiprocessing.Process. + It is the process from which the user of PythonTurtle works; + It defines all the turtle commands (i.e. go, turn, width, etc.). + Then it runs a shelltoprocess.Console which connects to the shell + in the main application window, allowing the user to control + this process. + """ + def __init__(self,*args,**kwargs): + multiprocessing.Process.__init__(self,*args,**kwargs) + + self.daemon=True + + + self.turtle_queue=multiprocessing.Queue() + self.queue_pack= [multiprocessing.Queue() for i in range(4)] + + + """ + Constants: + """ + self.FPS=25 + self.FRAME_TIME=1/float(self.FPS) + + + + def send_report(self): + """ + Sends a "turtle report" to the TurtleWidget. + By sending turtle reports every time the turtle does anything, + the TurtleWidget can always know where the turtle is going + and draw graphics accordingly. + """ + #self.turtle.fingerprint = random.randint(0,10000) + self.turtle_queue.put(self.turtle) + #log(self.turtle.__dict__) + + def run(self): + + self.turtle=Turtle() + + def go(distance): + """ + Makes the turtle walk the specified distance. Use a negative number + to walk backwards. + """ + if distance==0: return + sign=1 if distance>0 else -1 + distance=copy.copy(abs(distance)) + distance_gone=0 + distance_per_frame=self.FRAME_TIME*self.turtle.SPEED + steps=int(math.ceil(distance/float(distance_per_frame))) + angle=from_my_angle(self.turtle.orientation) + unit_vector=Vector((math.sin(angle),math.cos(angle)))*sign + step=distance_per_frame*unit_vector + for i in range(steps-1): + with smartsleep.Sleeper(self.FRAME_TIME): + self.turtle.pos+=step + self.send_report() + distance_gone+=distance_per_frame + + last_distance=distance-distance_gone + last_sleep=last_distance/float(self.turtle.SPEED) + with smartsleep.Sleeper(last_sleep): + last_step=unit_vector*last_distance + self.turtle.pos+=last_step + self.send_report() + + def turn(angle): + """ + Makes the turtle turn. Specify angle in degrees. A positive + number turns clockwise, a negative number turns counter-clockwise. + """ + if angle==0: return + sign=1 if angle>0 else -1 + angle=copy.copy(abs(angle)) + angle_gone=0 + angle_per_frame=self.FRAME_TIME*self.turtle.ANGULAR_SPEED + steps=int(math.ceil(angle/float(angle_per_frame))) + step=angle_per_frame*sign + for i in range(steps-1): + with smartsleep.Sleeper(self.FRAME_TIME): + self.turtle.orientation+=step + self.send_report() + angle_gone+=angle_per_frame + + last_angle=angle-angle_gone + last_sleep=last_angle/float(self.turtle.ANGULAR_SPEED) + with smartsleep.Sleeper(last_sleep): + last_step=last_angle*sign + self.turtle.orientation+=last_step + self.send_report() + + def color(color): + """ + Sets the color of the turtle's pen. Specify a color as a string. + + Examples: + color("white") + color("green") + color("#00FFCC") + """ + #if not valid_color(color): + # raise StandardError(color+" is not a valid color.") + self.turtle.color=color + self.send_report() + + def width(width): + """ + Sets the width of the turtle's pen. Width must be a positive number. + """ + #assert 0 < width + self.turtle.width = width + self.send_report() + + def visible(visible=True): + """ + By default, makes the turtle visible. You may specify a boolean + value, e.g. visible(False) will make the turtle invisible. + """ + self.turtle.visible = visible + self.send_report() + + def invisible(): + """ + Makes the turtle invisible. + """ + self.turtle.visible=False + self.send_report() + + def pen_down(pen_down=True): + """ + By default, puts the pen in the "down" position, making the turtle + leave a trail when walking. You may specify a boolean value, e.g. + pen_down(False) will put the pen in the "up" position. + """ + self.turtle.pen_down = pen_down + self.send_report() + + def pen_up(): + """ + Puts the pen in the "up" position, making the turtle not leave a + trail when walking. + """ + self.turtle.pen_down = False + self.send_report() + + def is_visible(): + """ + Returns whether the turtle is visible. + """ + return self.turtle.visible + + def is_pen_down(): + """ + Returns whether the pen is in the "down" position. + """ + return self.turtle.pen_down + + def sin(angle): + """ + Calculates sine, with the angle specified in degrees. + """ + return math.sin(angles.deg_to_rad(angle)) + + def cos(angle): + """ + Calculates cosine, with the angle specified in degrees. + """ + return math.cos(angles.deg_to_rad(angle)) + + def clear(): + """ + Clears the screen, making it all black again. + """ + self.turtle.clear=True + self.send_report() + time.sleep(0.1) + self.turtle.clear=False + self.send_report() + + """ + Had trouble implementing `home`. + I couldn't control when the turtle would actually draw a line home. + + def home(): + #\""" + Places the turtle at the center of the screen, facing upwards. + #\""" + old_pen_down = self.turtle.pen_down + pen_up() # Sends a report as well + self.send_report() + self.turtle.pos = Vector((0, 0)) + self.turtle.orientation = 180 + self.send_report() + time.sleep(3) + pen_down(old_pen_down) + """ + + def reset(): + """ + Resets all the turtle's properties and clears the screen. + """ + self.turtle = Turtle() + clear() + + #go(200) + #turn(90) + #go(200) + #turn(90) + #go(200) + #turn(90) + #go(200) + #turn(90) + + locals_for_console={"go": go, "turn": turn, "color": color, + "width": width, "visible": visible, + "invisible": invisible, "pen_down": pen_down, + "pen_up": pen_up, "is_visible": is_visible, + "is_pen_down": is_pen_down, "sin": sin, "cos": cos, + "turtle": self.turtle, "clear": clear, + "reset": reset} + + + """ + A little thing I tried doing for checking if a color is + valid before setting it to the turtle. Didn't work. + import wx; app=wx.App(); + def valid_color(color): + return not wx.Pen(color).GetColour()==wx.Pen("malformed").GetColour() + """ + + self.console = console.Console(queue_pack=self.queue_pack,locals=locals_for_console) + #import cProfile; cProfile.runctx("console.interact()", globals(), locals()) + self.console.interact() + #sys.stdout.flush() diff --git a/turtlewidget.py b/turtlewidget.py new file mode 100644 index 0000000..4bcf0eb --- /dev/null +++ b/turtlewidget.py @@ -0,0 +1,134 @@ +""" +TurtleWidget is defined in this module, see its documentation. +""" + +import time +import Queue +import math + +from gi.repository import Gtk, Gdk, GObject +import cairo + +from vector import Vector +from my_turtle import * +import misc.dumpqueue as dumpqueue +from misc.fromresourcefolder import from_resource_folder + +BITMAP_X = 1200 +BITMAP_Y = 900 + +class TurtleWidget(Gtk.DrawingArea): + """ + A Gtk Drawing Area widget to display the turtle and all the drawings that + it made. + """ + def __init__(self,turtle_queue): + Gtk.DrawingArea.__init__(self) + + #self.BACKGROUND_COLOR = wx.Colour(212,208,200) + self.TURTLE_IMAGE = cairo.ImageSurface.create_from_png(from_resource_folder("turtle.png")) + self.turtle_w = self.TURTLE_IMAGE.get_width() + self.turtle_h = self.TURTLE_IMAGE.get_height() + self.turtle = Turtle() + + self.count = 0 + #bitmap=self.bitmap=wx.EmptyBitmapRGBA(2000,1200,BACKGROUND_COLOR[0],BACKGROUND_COLOR[1],BACKGROUND_COLOR[2],255) # todo: Change to something smarter? + #self.bitmap = wx.EmptyBitmap(*BITMAP_SIZE) + self.bitmap = cairo.ImageSurface(cairo.FORMAT_RGB24, BITMAP_X, BITMAP_Y) + self.connect("draw", self.on_paint) + GObject.timeout_add(30, self.on_idle) + #GObject.idle_add(self.on_idle) + + #self.Bind(wx.EVT_SIZE,self.on_size) + + self.turtle_queue = turtle_queue + + self.idle_block = False + + def on_paint(self, widget, cr): + """ + Paint event handler. Reads the turtle reports and draws graphics + accordingly. + """ + # Translate to (0, 0) is in the center + w = self.get_allocation().width + h = self.get_allocation().height + + cxt = cairo.Context(self.bitmap) # The drawing context for the bitmap + cxt.translate(w/2, h/2) + + turtle_reports=dumpqueue.dump_queue(self.turtle_queue) + for turtle_report in turtle_reports: + if turtle_report.pen_down is True: + cxt.set_source_rgb(*self.turtle.rgb_color) + cxt.set_line_width(self.turtle.width) + cxt.move_to(*(-self.turtle.pos)) + cxt.line_to(*(-turtle_report.pos)) + #if turtle_report.clear is True: + # brush=wx.Brush("black") + # dc.SetBackground(brush) + # dc.Clear() + self.turtle = turtle_report + cxt.stroke() + + if len(turtle_reports) > 0: + pass + #self.queue_draw() + + # Draw the bitmap + cr.set_source_surface(self.bitmap, 0, 0) + cr.paint() + + if self.turtle.visible: + p = -self.turtle.pos + x = p[0] + w/2 + y = p[1] + h/2 + cr.translate(x, y) + cr.rotate(math.radians(self.turtle.orientation - 180)) + cr.translate(-x, -y) + cr.set_source_surface(self.TURTLE_IMAGE, x - self.turtle_w/2, y - self.turtle_h/2) + cr.paint() + + + # Draw the turtle: + #if self.turtle.visible: + # new_pos = top_left_corner + from_my_pos(self.turtle.pos) + # draw_bitmap_to_dc_rotated(dc, self.TURTLE_IMAGE, from_my_angle(self.turtle.orientation), new_pos) + #dc.Destroy() + + + + def on_idle(self,e=None): + """ + Idle event handler. Checks whether there are any + pending turtle reports, and if there are tells the widget + to process them. + """ + if self.idle_block==True: return True + + if not self.turtle_queue.empty(): self.queue_draw() + return True + #wx.CallLater(30,self._clear_idle_block_and_do) # Should make the delay customizable? + #self.idle_block=True + + + def _clear_idle_block_and_do(self): + self.idle_block=False + event=wx.PyEvent() + event.SetEventType(wx.wxEVT_IDLE) + wx.PostEvent(self,event) + + def on_size(self,e=None): + self.Refresh() + + + +def draw_bitmap_to_dc_rotated( dc, bitmap, angle , point): + """ + Rotate a bitmap and write it to the supplied device context. + """ + img = bitmap.ConvertToImage() + img_centre = wx.Point( img.GetWidth()/2.0, img.GetHeight()/2.0 ) + img = img.Rotate( angle, img_centre , interpolating=True) + new_point=Vector(point)-Vector(img.GetSize())/2 + dc.DrawBitmapPoint( img.ConvertToBitmap(), new_point,useMask=True ) diff --git a/vector.py b/vector.py new file mode 100644 index 0000000..d205077 --- /dev/null +++ b/vector.py @@ -0,0 +1,143 @@ +""" +Implements mainly the Vector class. See its documentation. +""" +class VectorError(Exception): + """ + An exception to use with Vector + """ + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return repr(self.value) + + +class Vector(tuple): + """ + A vector. + """ + def __init__(self,seq): + tuple.__init__(seq) + + def __add__(self, other): + if not(isinstance(other,Vector)): + raise VectorError("right hand side is not a Vector") + return Vector(map(lambda x,y: x+y, self, other)) + + def __neg__(self): + return Vector(map(lambda x: -x, self)) + + def __pos__(self): + return self + + def __sub__(self, other): + return Vector(map(lambda x,y: x-y, self, other)) + + def __mul__(self, other): + if not(isinstance(other,int) or isinstance(other,float)): + raise VectorError("right hand side is illegal") + return Vector(map(lambda x: x*other, self)) + + + def __rmul__(self, other): + return (self*other) + + def __div__(self, other): + if not(isinstance(other,int) or isinstance(other,float)): + raise VectorError("right hand side is illegal") + return Vector(map(lambda x: x/other, self)) + + def __rdiv__(self, other): + raise VectorError("you sick pervert! you tried to divide something by a vector!") + + def __and__(self,other): + """ + this is a dot product, done like this: a&b + must use () around it because of fucked up operator precedence. + """ + if not(isinstance(other,Vector)): + raise VectorError("trying to do dot product of Vector with non-Vector") + """ + if self.dim()!=other.dim(): + raise("trying to do dot product of Vectors of unequal dimension!") + """ + d=self.dim() + s=0. + for i in range(d): + s+=self[i]*other[i] + return s + + def __rand__(self,other): + return self&other + + def __or__(self,other): + """ + cross product, defined only for 3D Vectors. goes like this: a|b + don't try this on non-3d Vectors. must use () around it because of fucked up operator precedence. + """ + a=self + b=other + return Vector([a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]]) + + def __ror__(self,other): + return -(self|other) + + def __abs__(self): + s=0. + for x in self: + s+=x**2 + return s**(1.0/2) + + def __iadd__(self,other): + self=self+other + return self + + def __isub__(self,other): + self=self-other + return self + + def __imul__(self,other): + self=self*other + return self + + def __idiv__(self,other): + self=self/other + return self + + def __iand__(self,other): + raise VectorError("please don't do &= with my Vectors, it confuses me") + + def __ior__(self,other): + self=self|other + return self + + def __repr__(self): + return "Vector("+tuple.__repr__(self)+")" + + def norm(self): + """ + gives the Vector, normalized + """ + return self/abs(self) + def dim(self): + return len(self) + + def copy(self): + return Vector(self) + + + + +################################################################################################ + +def zeros(n): + """ + Returns a zero Vector of length n. + """ + return Vector(map(lambda x: 0., range(n))) + +def ones(n): + """ + Returns a Vector of length n with all ones. + """ + return Vector(map(lambda x: 1., range(n))) |