From d082a08b7d60fa3b75f46940d772e176a6bdc046 Mon Sep 17 00:00:00 2001 From: Walter Bender Date: Sun, 06 May 2012 22:39:06 +0000 Subject: calling it by its proper name --- (limited to 'turtleblocks.py') diff --git a/turtleblocks.py b/turtleblocks.py new file mode 100755 index 0000000..cf02e51 --- /dev/null +++ b/turtleblocks.py @@ -0,0 +1,633 @@ +#!/usr/bin/env python +#Copyright (c) 2007-8, Playful Invention Company +#Copyright (c) 2008-12, Walter Bender +#Copyright (c) 2011 Collabora 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 pygtk +pygtk.require('2.0') +import gtk +import cairo + +import getopt +import sys +import os +import os.path +import cStringIO +import errno +import ConfigParser + +try: + # Try to use XDG Base Directory standard for config files. + import xdg.BaseDirectory + CONFIG_HOME = os.path.join(xdg.BaseDirectory.xdg_config_home, 'turtleart') +except ImportError, e: + # Default to `.config` per the spec. + CONFIG_HOME = os.path.expanduser(os.path.join('~', '.config', 'turtleart')) + +argv = sys.argv[:] # Workaround for import behavior of gst in tagplay +sys.argv[1:] = [] # Execution of import gst cannot see '--help' or '-h' + +import gettext + +from TurtleArt.taconstants import OVERLAY_LAYER, DEFAULT_TURTLE_COLORS +from TurtleArt.tautils import data_to_string, data_from_string, get_save_name +from TurtleArt.tawindow import TurtleArtWindow +from TurtleArt.taexporthtml import save_html +from TurtleArt.taexportlogo import save_logo + +from util.menubuilder import MenuBuilder + + +class TurtleMain(): + ''' Launch Turtle Art in GNOME (from outside of Sugar). ''' + _INSTALL_PATH = '/usr/share/sugar/activities/TurtleArt.activity' + _ALTERNATIVE_INSTALL_PATH = \ + '/usr/local/share/sugar/activities/TurtleArt.activity' + _ICON_SUBPATH = 'images/turtle.png' + _GNOME_PLUGIN_SUBPATH = 'gnome_plugins' + + def __init__(self): + self._abspath = os.path.abspath('.') + self._execdirname = self._get_execution_dir() + if self._execdirname is not None: + os.chdir(self._execdirname) + file_activity_info = ConfigParser.ConfigParser() + activity_info_path = os.path.abspath('./activity/activity.info') + file_activity_info.read(activity_info_path) + bundle_id = file_activity_info.get('Activity', 'bundle_id') + gettext.textdomain(bundle_id) + global _ + _ = gettext.gettext + _HELP_MSG = 'turtleart.py: ' + _('usage is') + ''' + \tturtleart.py + \tturtleart.py project.ta + \tturtleart.py --output_png project.ta + \tturtleart.py -o project + \tturtleart.py --run project.ta + \tturtleart.py -r project''' + self._init_vars() + self._parse_command_line() + self._ensure_sugar_paths() + self._plugins = [] + + if self._output_png: + # Outputing to file, so no need for a canvas + self.canvas = None + self._build_window(interactive=False) + self._draw_and_quit() + else: + self._read_initial_pos() + self._init_plugins() + self._setup_gtk() + self._build_window() + self._run_plugins() + self._start_gtk() + + def get_config_home(self): + return CONFIG_HOME + + def _get_gnome_plugin_home(self): + ''' Use plugin directory associated with execution path. ''' + if os.path.exists(os.path.join(self._execdirname, + self._GNOME_PLUGIN_SUBPATH)): + return os.path.join(self._execdirname, self._GNOME_PLUGIN_SUBPATH) + else: + return None + + def _get_plugin_candidates(self, path): + ''' Look for plugin files in plugin directory. ''' + plugin_files = [] + if path is not None: + candidates = os.listdir(path) + for c in candidates: + if c[-10:] == '_plugin.py' and c[0] != '#' and c[0] != '.': + plugin_files.append(c.split('.')[0]) + return plugin_files + + def _init_plugins(self): + ''' Try launching any plugins we may have found. ''' + for p in self._get_plugin_candidates(self._get_gnome_plugin_home()): + P = p.capitalize() + f = \ + "def f(self): from gnome_plugins.%s import %s; return %s(self)" % (p, P, P) + plugin = {} + try: + exec f in globals(), plugin + self._plugins.append(plugin.values()[0](self)) + except ImportError, e: + print 'failed to import %s: %s' % (P, str(e)) + + def _run_plugins(self): + ''' Tell the plugin about the TurtleWindow instance. ''' + for p in self._plugins: + p.set_tw(self.tw) + + def _mkdir_p(self, path): + '''Create a directory in a fashion similar to `mkdir -p`.''' + try: + os.makedirs(path) + except OSError, exc: + if exc.errno == errno.EEXIST: + pass + else: + raise + + def _makepath(self, path): + ''' Make a path if it doesn't previously exist ''' + from os import makedirs + from os.path import normpath, dirname, exists + + dpath = normpath(dirname(path)) + if not exists(dpath): + makedirs(dpath) + + def _start_gtk(self): + ''' Get a main window set up. ''' + self.win.connect('configure_event', self.tw.update_overlay_position) + self.tw.parent = self.win + if self._ta_file is None: + self.tw.load_start() + else: + print self._ta_file + self.tw.load_start(self._ta_file) + self.tw.lc.trace = 0 + if self._run_on_launch: + self._do_run_cb() + gtk.main() + + def _draw_and_quit(self): + ''' Non-interactive mode: run the project, save it to a file + and quit. ''' + self.tw.load_start(self._ta_file) + self.tw.lc.trace = 0 + self.tw.run_button(0) + self.tw.save_as_image(self._ta_file) + + def _build_window(self, interactive=True): + ''' Initialize the TurtleWindow instance. ''' + if interactive: + win = self.canvas.get_window() + cr = 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)) + self.tw = TurtleArtWindow(self.canvas, self._execdirname, + turtle_canvas=self.turtle_canvas) + self.tw.save_folder = os.path.expanduser('~') + + def _init_vars(self): + ''' If we are invoked to start a project from Gnome, we should make + sure our current directory is TA's source dir. ''' + self._ta_file = None + self._output_png = False + self._run_on_launch = False + self.current_palette = 0 + self.scale = 2.0 + self.tw = None + + def _parse_command_line(self): + ''' Try to make sense of the command-line arguments. ''' + try: + opts, args = getopt.getopt(argv[1:], 'hor', + ['help', 'output_png', 'run']) + except getopt.GetoptError, err: + print str(err) + print self._HELP_MSG + sys.exit(2) + for o, a in opts: + if o in ('-h', '--help'): + print self._HELP_MSG + sys.exit() + if o in ('-o', '--output_png'): + self._output_png = True + elif o in ('-r', '--run'): + self._run_on_launch = True + else: + assert False, _('No option action:') + ' ' + o + if args: + self._ta_file = args[0] + + if len(args) > 1 or self._output_png and self._ta_file is None: + print self._HELP_MSG + sys.exit() + + if self._ta_file is not None: + if not self._ta_file.endswith(('.ta')): + self._ta_file += '.ta' + if not os.path.exists(self._ta_file): + self._ta_file = os.path.join(self._abspath, self._ta_file) + if not os.path.exists(self._ta_file): + assert False, ('%s: %s' % (self._ta_file, + _('File not found'))) + + def _ensure_sugar_paths(self): + ''' Make sure Sugar paths are present. ''' + tapath = os.path.join(os.environ['HOME'], '.sugar', 'default', + 'org.laptop.TurtleArtActivity') + map(self._makepath, (os.path.join(tapath, 'data/'), + os.path.join(tapath, 'instance/'))) + + def _read_initial_pos(self): + ''' Read saved configuration. ''' + try: + data_file = open(os.path.join(CONFIG_HOME, 'turtleartrc'), 'r') + except IOError: + # Opening the config file failed + # We'll assume it needs to be created + try: + self._mkdir_p(CONFIG_HOME) + data_file = open(os.path.join(CONFIG_HOME, 'turtleartrc'), + 'a+') + except IOError, e: + # We can't write to the configuration file, use + # a faux file that will persist for the length of + # the session. + print _('Configuration directory not writable: %s') % (e) + data_file = cStringIO.StringIO() + data_file.write(str(50) + '\n') + data_file.write(str(50) + '\n') + data_file.write(str(800) + '\n') + data_file.write(str(550) + '\n') + data_file.seek(0) + try: + self.x = int(data_file.readline()) + self.y = int(data_file.readline()) + self.width = int(data_file.readline()) + self.height = int(data_file.readline()) + except ValueError: + self.x = 50 + self.y = 50 + self.width = 800 + self.height = 550 + + def _setup_gtk(self): + ''' Set up a scrolled window in which to run Turtle Blocks. ''' + win = gtk.Window(gtk.WINDOW_TOPLEVEL) + win.set_default_size(self.width, self.height) + win.move(self.x, self.y) + win.maximize() + win.set_title(_('Turtle Art')) + if os.path.exists(os.path.join(self._execdirname, self._ICON_SUBPATH)): + win.set_icon_from_file(os.path.join(self._execdirname, + self._ICON_SUBPATH)) + win.connect('delete_event', self._quit_ta) + + vbox = gtk.VBox(False, 0) + win.add(vbox) + vbox.show() + + menu_bar = self._get_menu_bar() + vbox.pack_start(menu_bar, False, False, 2) + menu_bar.show() + + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw.show() + canvas = gtk.DrawingArea() + width = gtk.gdk.screen_width() * 2 + height = gtk.gdk.screen_height() * 2 + canvas.set_size_request(width, height) + sw.add_with_viewport(canvas) + canvas.show() + vbox.pack_end(sw, True, True) + + win.show_all() + self.win = win + self.canvas = canvas + + def _get_menu_bar(self): + ''' Instead of Sugar toolbars, use GNOME menus. ''' + menu = gtk.Menu() + MenuBuilder.make_menu_item(menu, _('New'), self._do_new_cb) + MenuBuilder.make_menu_item(menu, _('Open'), self._do_open_cb) + MenuBuilder.make_menu_item(menu, _('Save'), self._do_save_cb) + MenuBuilder.make_menu_item(menu, _('Save as'), self._do_save_as_cb) + MenuBuilder.make_menu_item(menu, _('Save as image'), + self._do_save_picture_cb) + MenuBuilder.make_menu_item(menu, _('Save as HTML'), + self._do_save_html_cb) + MenuBuilder.make_menu_item(menu, _('Save as Logo'), + self._do_save_logo_cb) + MenuBuilder.make_menu_item(menu, _('Quit'), self._quit_ta) + activity_menu = MenuBuilder.make_sub_menu(menu, _('File')) + + menu = gtk.Menu() + MenuBuilder.make_menu_item(menu, _('Cartesian coordinates'), + self._do_cartesian_cb) + MenuBuilder.make_menu_item(menu, _('Polar coordinates'), + self._do_polar_cb) + MenuBuilder.make_menu_item(menu, _('Rescale coordinates'), + self._do_rescale_cb) + MenuBuilder.make_menu_item(menu, _('Grow blocks'), + self._do_resize_cb, 1.5) + MenuBuilder.make_menu_item(menu, _('Shrink blocks'), + self._do_resize_cb, 0.667) + MenuBuilder.make_menu_item(menu, _('Reset block size'), + self._do_resize_cb, -1) + MenuBuilder.make_menu_item(menu, _('Turn off hover help'), + self._do_hover_help_off_cb) + MenuBuilder.make_menu_item(menu, _('Turn on hover help'), + self._do_hover_help_on_cb) + view_menu = MenuBuilder.make_sub_menu(menu, _('View')) + + menu = gtk.Menu() + MenuBuilder.make_menu_item(menu, _('Copy'), self._do_copy_cb) + MenuBuilder.make_menu_item(menu, _('Paste'), self._do_paste_cb) + edit_menu = MenuBuilder.make_sub_menu(menu, _('Edit')) + + menu = gtk.Menu() + MenuBuilder.make_menu_item(menu, _('Show palette'), + self._do_palette_cb) + MenuBuilder.make_menu_item(menu, _('Hide palette'), + self._do_hide_palette_cb) + MenuBuilder.make_menu_item(menu, _('Show/hide blocks'), + self._do_hideshow_cb) + tool_menu = MenuBuilder.make_sub_menu(menu, _('Tools')) + + menu = gtk.Menu() + MenuBuilder.make_menu_item(menu, _('Clean'), self._do_eraser_cb) + MenuBuilder.make_menu_item(menu, _('Run'), self._do_run_cb) + MenuBuilder.make_menu_item(menu, _('Step'), self._do_step_cb) + MenuBuilder.make_menu_item(menu, _('Debug'), self._do_trace_cb) + MenuBuilder.make_menu_item(menu, _('Stop'), self._do_stop_cb) + turtle_menu = MenuBuilder.make_sub_menu(menu, _('Turtle')) + + menu_bar = gtk.MenuBar() + menu_bar.append(activity_menu) + menu_bar.append(edit_menu) + menu_bar.append(view_menu) + menu_bar.append(tool_menu) + menu_bar.append(turtle_menu) + + # Add menus for plugins + for p in self._plugins: + menu_bar.append(p.get_menu()) + return menu_bar + + def _quit_ta(self, widget=None, e=None): + ''' Save changes on exit ''' + project_empty = self.tw.is_project_empty() + if not project_empty: + if self.tw.is_new_project(): + self._show_save_dialog(True) + else: + if self.tw.project_has_changed(): + self._show_save_dialog(False) + for plugin in self.tw._plugins: + if hasattr(plugin, 'quit'): + plugin.quit() + gtk.main_quit() + exit() + + def _show_save_dialog(self, new_project=True): + ''' Dialog for save project ''' + dlg = gtk.MessageDialog(parent=None, type=gtk.MESSAGE_INFO, + buttons=gtk.BUTTONS_OK_CANCEL, + message_format=_( + 'You have unsaved work. Would you like to save before quitting?')) + dlg.set_title(_('Save project?')) + dlg.set_property('skip-taskbar-hint', False) + + resp = dlg.run() + dlg.destroy() + if resp == gtk.RESPONSE_OK: + if new_project: + self._save_as() + else: + self._save_changes() + + def _do_new_cb(self, widget): + ''' Callback for new project. ''' + self.tw.new_project() + self.tw.load_start() + + def _do_open_cb(self, widget): + ''' Callback for open project. ''' + self.tw.load_file(True) + + def _do_save_cb(self, widget): + ''' Callback for save project. ''' + self.tw.save_file() + + def _do_save_as_cb(self, widget): + ''' Callback for save-as project. ''' + self._save_as() + + def _save_as(self): + ''' Save as is called from callback and quit ''' + self.tw.save_file_name = None + self.tw.save_file() + + def _save_changes(self): + ''' Save changes to current project ''' + self.tw.save_file_name = None + self.tw.save_file(self.tw._loaded_project) + + def _do_save_picture_cb(self, widget): + ''' Callback for save canvas. ''' + self.tw.save_as_image() + + def _do_save_html_cb(self, widget): + ''' Callback for save project to HTML. ''' + html = save_html(self, self.tw, False) + if len(html) == 0: + return + save_type = '.html' + if len(self.tw.saved_pictures) > 0: + if self.tw.saved_pictures[0].endswith(('.svg')): + save_type = '.xml' + filename, self.tw.load_save_folder = get_save_name(save_type, + self.tw.load_save_folder, 'portfolio') + f = file(filename, 'w') + f.write(html) + f.close() + self.tw.saved_pictures = [] + + def _do_save_logo_cb(self, widget): + ''' Callback for save project to Logo. ''' + logocode = save_logo(self.tw) + if len(logocode) == 0: + return + save_type = '.lg' + filename, self.tw.load_save_folder = get_save_name(save_type, + self.tw.load_save_folder, 'logosession') + f = file(filename, 'w') + f.write(logocode) + f.close() + + def _do_resize_cb(self, widget, factor): + ''' Callback to resize blocks. ''' + if factor == -1: + self.tw.block_scale = 2.0 + else: + self.tw.block_scale *= factor + self.tw.resize_blocks() + + def _do_cartesian_cb(self, button): + ''' Callback to display/hide Cartesian coordinate overlay. ''' + self.tw.set_cartesian(True) + + def _do_polar_cb(self, button): + ''' Callback to display/hide Polar coordinate overlay. ''' + self.tw.set_polar(True) + + def _do_rescale_cb(self, button): + ''' Callback to rescale coordinate space. ''' + if self.tw.coord_scale == 1: + self.tw.coord_scale = self.tw.height / 200 + self.tw.eraser_button() + if self.tw.cartesian is True: + self.tw.overlay_shapes['Cartesian_labeled'].hide() + self.tw.overlay_shapes['Cartesian'].set_layer(OVERLAY_LAYER) + else: + self.tw.coord_scale = 1 + self.tw.eraser_button() + if self.tw.cartesian is True: + self.tw.overlay_shapes['Cartesian'].hide() + self.tw.overlay_shapes['Cartesian_labeled'].set_layer( + OVERLAY_LAYER) + + def _do_hover_help_on_cb(self, button): + ''' Turn hover help on ''' + self.tw.no_help = False + + def _do_hover_help_off_cb(self, button): + ''' Turn hover help off ''' + self.tw.no_help = True + self.tw.last_label = None + self.tw.status_spr.hide() + + def _do_palette_cb(self, widget): + ''' Callback to show/hide palette of blocks. ''' + self.tw.show_palette(self.current_palette) + self.current_palette += 1 + if self.current_palette == len(self.tw.palettes): + self.current_palette = 0 + + def _do_hide_palette_cb(self, widget): + ''' Hide the palette of blocks. ''' + self.tw.hide_palette() + + def _do_hideshow_cb(self, widget): + ''' Hide/show the blocks. ''' + self.tw.hideshow_button() + + def _do_eraser_cb(self, widget): + ''' Callback for eraser button. ''' + self.tw.eraser_button() + return + + def _do_run_cb(self, widget=None): + ''' Callback for run button (rabbit). ''' + self.tw.lc.trace = 0 + self.tw.hideblocks() + self.tw.run_button(0, running_from_button_push=True) + return + + def _do_step_cb(self, widget): + ''' Callback for step button (turtle). ''' + self.tw.lc.trace = 1 + self.tw.run_button(3, running_from_button_push=True) + return + + def _do_trace_cb(self, widget): + ''' Callback for debug button (bug). ''' + self.tw.lc.trace = 1 + self.tw.run_button(9, running_from_button_push=True) + return + + def _do_stop_cb(self, widget): + ''' Callback for stop button. ''' + self.tw.lc.trace = 0 + self.tw.stop_button() + return + + def _do_copy_cb(self, button): + ''' Callback for copy button. ''' + clipBoard = gtk.Clipboard() + data = self.tw.assemble_data_to_save(False, False) + if data is not []: + text = data_to_string(data) + clipBoard.set_text(text) + + def _do_paste_cb(self, button): + ''' Callback for paste button. ''' + clipBoard = gtk.Clipboard() + text = clipBoard.wait_for_text() + if text is not None: + if self.tw.selected_blk is not None and\ + self.tw.selected_blk.name == 'string': + for i in text: + self.tw.process_alphanumeric_input(i, -1) + self.tw.selected_blk.resize() + else: + self.tw.process_data(data_from_string(text), + self.tw.paste_offset) + self.tw.paste_offset += 20 + + def _window_event(self, event, data): + ''' Callback for resize event. ''' + data_file = open('.turtleartrc', 'w') + data_file.write(str(data.x) + '\n') + data_file.write(str(data.y) + '\n') + data_file.write(str(data.width) + '\n') + data_file.write(str(data.height) + '\n') + + def nick_changed(self, nick): + ''' TODO: Rename default turtle in dictionary ''' + pass + + def color_changed(self, colors): + ''' Reskin turtle with collaboration colors ''' + turtle = self.tw.turtles.get_turtle(self.tw.default_turtle_name) + try: + turtle.colors = colors.split(',') + except: + turtle.colors = DEFAULT_TURTLE_COLORS + turtle.custom_shapes = True # Force regeneration of shapes + turtle.reset_shapes() + turtle.show() + + def _get_execution_dir(self): + ''' From whence is the program being executed? ''' + dirname = os.path.dirname(__file__) + if dirname == '': + if os.path.exists(os.path.join('~', 'Activities', + 'TurtleArt.activity')): + return os.path.join('~', 'Activities', 'TurtleArt.activity') + elif os.path.exists(self._INSTALL_PATH): + return self._INSTALL_PATH + elif os.path.exists(self._ALTERNATIVE_INSTALL_PATH): + return self._ALTERNATIVE_INSTALL_PATH + else: + return os.path.abspath('.') + else: + return os.path.abspath(dirname) + + +if __name__ == '__main__': + TurtleMain() -- cgit v0.9.1