# Copyright (C) 2009, Tutorius.org # # 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 """ This module renders a widget containing a graphical representation of a tutorial and acts as a creator proxy as it has some editing functionality. """ import gtk import cairo from math import pi as PI PI2 = PI/2 import rsvg from sugar.bundle import activitybundle from sugar.tutorius import addon from sugar.graphics import icon from sugar.tutorius.actions import Action import os # FIXME ideally, apps scale correctly and we should use proportional positions X_WIDTH = 800 X_HEIGHT = 600 ACTION_WIDTH = 100 ACTION_HEIGHT = 70 # block look BLOCK_PADDING = 5 BLOCK_WIDTH = 100 BLOCK_CORNERS = 10 BLOCK_INNER_PAD = 10 SNAP_WIDTH = BLOCK_WIDTH - BLOCK_PADDING - BLOCK_INNER_PAD*2 SNAP_HEIGHT = SNAP_WIDTH*X_HEIGHT/X_WIDTH SNAP_SCALE = float(SNAP_WIDTH)/X_WIDTH class Viewer(object): """ Renders a tutorial as a sequence of blocks, each block representing either an action or an event (transition). Current Viewer implementation lacks viewport management; having many objects in a tutorial will not render properly. """ def __init__(self, tutorial, creator): super(Viewer, self).__init__() self._tutorial = tutorial self._creator = creator self.alloc = None self.click_pos = None self.drag_pos = None self.selection = set() self.win = gtk.Window(gtk.WINDOW_TOPLEVEL) self.win.set_size_request(400, 200) self.win.set_gravity(gtk.gdk.GRAVITY_SOUTH_WEST) self.win.show() self.win.set_deletable(False) self.win.move(0, 0) vbox = gtk.ScrolledWindow() self.win.add(vbox) canvas = gtk.DrawingArea() vbox.add_with_viewport(canvas) canvas.set_app_paintable(True) canvas.connect_after("expose-event", self.on_viewer_expose, tutorial) canvas.add_events(gtk.gdk.BUTTON_PRESS_MASK \ |gtk.gdk.BUTTON_MOTION_MASK \ |gtk.gdk.BUTTON_RELEASE_MASK \ |gtk.gdk.KEY_PRESS_MASK) canvas.connect('button-press-event', self._on_click) # drag-select disabled, for now #canvas.connect('motion-notify-event', self._on_drag) canvas.connect('button-release-event', self._on_drag_end) canvas.connect('key-press-event', self._on_key_press) canvas.set_flags(gtk.HAS_FOCUS|gtk.CAN_FOCUS) canvas.grab_focus() self.win.show_all() canvas.set_size_request(2048, 180) # FIXME def destroy(self): """ Destroy ui resources associated with this object. """ self.win.destroy() def _paint_state(self, ctx, tutorial): """ Paints a tutorius fsm state in a cairo context. Final context state will be shifted by the size of the graphics. """ block_width = BLOCK_WIDTH - BLOCK_PADDING block_max_height = self.alloc.height new_insert_point = None state_name = tutorial.INIT # FIXME: get app when we have a model that supports it cur_app = 'Calculate' app_start = ctx.get_matrix() while state_name: new_app = 'Calculate' if new_app != cur_app: ctx.save() ctx.set_matrix(app_start) self._render_app_hints(ctx, cur_app) ctx.restore() app_start = ctx.get_matrix() ctx.translate(BLOCK_PADDING, 0) cur_app = new_app action_list = tutorial.get_action_dict(state_name).items() if action_list: local_height = (block_max_height - BLOCK_PADDING)\ / len(action_list) - BLOCK_PADDING ctx.save() for action_name, action in action_list: origin = tuple(ctx.get_matrix())[-2:] if self.click_pos and \ self.click_pos[0]-BLOCK_WIDTHorigin[0]: self.selection.add((action_name, action)) self.render_action(ctx, block_width, local_height, action) ctx.translate(0, local_height+BLOCK_PADDING) ctx.restore() ctx.translate(BLOCK_WIDTH, 0) # insertion cursor painting made from two opposed triangles # joined by a line. if state_name == self._creator.get_insertion_point(): ctx.save() bp2 = BLOCK_PADDING/2 ctx.move_to(-bp2, 0) ctx.line_to(-BLOCK_PADDING-bp2, -BLOCK_PADDING) ctx.line_to(bp2, -BLOCK_PADDING) ctx.line_to(-bp2, 0) ctx.line_to(-bp2, block_max_height-2*BLOCK_PADDING) ctx.line_to(bp2, block_max_height-BLOCK_PADDING) ctx.line_to(-BLOCK_PADDING-bp2, block_max_height-BLOCK_PADDING) ctx.line_to(-bp2, block_max_height-2*BLOCK_PADDING) ctx.line_to(-bp2, BLOCK_PADDING) ctx.set_source_rgb(1.0, 1.0, 0.0) ctx.stroke_preserve() ctx.fill() ctx.restore() event_list = tutorial.get_transition_dict(state_name).items() if event_list: local_height = (block_max_height - BLOCK_PADDING)\ /len(event_list) - BLOCK_PADDING ctx.save() for transition_name, transition in event_list: origin = tuple(ctx.get_matrix())[-2:] if self.click_pos and \ self.click_pos[0]-BLOCK_WIDTHorigin[0]: self.selection.add((transition_name, transition)) self.render_event(ctx, block_width, local_height, event=transition[0]) ctx.translate(0, local_height+BLOCK_PADDING) ctx.restore() ctx.translate(BLOCK_WIDTH, 0) if (not new_insert_point) and self.click_pos: origin = tuple(ctx.get_matrix())[-2:] if self.click_pos[0]