#!/usr/bin/env python import sys import os SUGAR_PREFIX = os.getenv('SUGAR_PREFIX') sys.path.append(SUGAR_PREFIX+'/lib/python2.6/site-packages') import gtk, gtk.gdk import cairo from math import pi as PI PI2 = PI/2 import rsvg from sugar.bundle import activitybundle ACTIVITY_PATH = '%s/share/sugar/activities/'%SUGAR_PREFIX #ACTIVITY_PATH = os.getenv('SUGAR_BUNDLE_PATH') # 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 # load data structure states = dict( INIT=dict( next='state1', app='Browse', actions=[dict(name='message', pos=[20,5])], events=[{'name':'click', 'pos':(200,200)}], ), state1=dict( next='state2', app='Browse', actions=[dict(name='mock')], events=[{'name':'click', 'pos':(200,100)},{'pos':(200,200), 'name':'keyboard'}], ), state2=dict( next='state3', app='Write', actions=[dict(name='flash', pos=[2,5]), dict(name='text', pos=[400,500])], events=[{'name':'keyboard'}], ), state3=dict( next='state4', app='Write', actions=[dict(name='message', pos=[2,5]), dict(name='hilight', pos=[600,100]), dict(name='message', pos=[300,500])], events=[{'name':'click'}], ), state4=dict( app='Browse', next=None, actions=[dict(name='message', pos=[200,5])], events=[{'name':'scratch&smell'}], ), ) def _paint_state(ctx, states): """ 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 = alloc.height cur_state = 'INIT' cur_app = states[cur_state]['app'] app_start = ctx.get_matrix() while cur_state: state = states[cur_state] new_app = state['app'] if new_app != cur_app: ctx.save() ctx.set_matrix(app_start) _render_app_hints(ctx, cur_app) ctx.restore() app_start = ctx.get_matrix() ctx.translate(BLOCK_PADDING, 0) cur_app = new_app local_height = (block_max_height - BLOCK_PADDING)/len(state['actions']) - BLOCK_PADDING ctx.save() for action in state['actions']: origin = tuple(ctx.get_matrix())[-2:] if click_pos and \ click_pos[0]-BLOCK_WIDTHorigin[0]: selection.append(action) render_action(ctx, block_width, local_height, action) ctx.translate(0, local_height+BLOCK_PADDING) ctx.restore() ctx.translate(BLOCK_WIDTH, 0) local_height = (block_max_height - BLOCK_PADDING)/len(state['events']) - BLOCK_PADDING ctx.save() for event in state['events']: origin = tuple(ctx.get_matrix())[-2:] if click_pos and \ click_pos[0]-BLOCK_WIDTHorigin[0]: selection.append(event) render_event(ctx, block_width, local_height, event) ctx.translate(0, local_height+BLOCK_PADDING) ctx.restore() ctx.translate(BLOCK_WIDTH, 0) # FIXME use special notation for happy path, not chaining cur_state = state['next'] yield True ctx.set_matrix(app_start) _render_app_hints(ctx, cur_app) yield False alloc = None click_pos = None drag_pos = None selection = [] def _render_snapshot(ctx, elem): ctx.set_source_rgba(1.0, 1.0, 1.0, 0.5) ctx.rectangle(0, 0, SNAP_WIDTH, SNAP_HEIGHT) ctx.fill_preserve() ctx.stroke() pos = elem.get('pos') if pos: # FIXME this size approximation is fine, but I believe we could # do better. ctx.scale(SNAP_SCALE, SNAP_SCALE) ctx.rectangle(pos[0], pos[1], ACTION_WIDTH, ACTION_HEIGHT) ctx.fill_preserve() ctx.stroke() def _render_app_hints(ctx, appname): ctx.set_source_rgb(0.0, 0.0, 0.0) ctx.set_dash((1,1,0,0), 1) ctx.move_to(0, 0) ctx.line_to(0, alloc.height) ctx.stroke() ctx.set_dash(tuple(), 1) icon_path = activitybundle.ActivityBundle( os.path.join(ACTIVITY_PATH, appname+'.activity')).get_icon() icon = rsvg.Handle(icon_path) ctx.save() ctx.translate(-15, 0) ctx.scale(0.5, 0.5) icon_surf = icon.render_cairo(ctx) ctx.restore() def render_action(ctx, width, height, action): ctx.save() inner_width = width-(BLOCK_CORNERS<<1) inner_height = height-(BLOCK_CORNERS<<1) paint_border = ctx.rel_line_to filling = cairo.LinearGradient(0, 0, 0, inner_height) if action not in selection: filling.add_color_stop_rgb(0.0, 0.7, 0.7, 1.0) filling.add_color_stop_rgb(1.0, 0.1, 0.1, 0.8) else: filling.add_color_stop_rgb(0.0, 0.4, 0.4, 0.8) filling.add_color_stop_rgb(1.0, 0.0, 0.0, 0.5) tracing = cairo.LinearGradient(0, 0, 0, inner_height) tracing.add_color_stop_rgb(0.0, 1.0, 1.0, 1.0) tracing.add_color_stop_rgb(1.0, 0.2, 0.2, 0.2) ctx.move_to(BLOCK_CORNERS, 0) paint_border(inner_width, 0) ctx.arc(inner_width+BLOCK_CORNERS, BLOCK_CORNERS, BLOCK_CORNERS, -PI2, 0.0) ctx.arc(inner_width+BLOCK_CORNERS, inner_height+BLOCK_CORNERS, BLOCK_CORNERS, 0.0, PI2) ctx.arc(BLOCK_CORNERS, inner_height+BLOCK_CORNERS, BLOCK_CORNERS, PI2, PI) ctx.arc(BLOCK_CORNERS, BLOCK_CORNERS, BLOCK_CORNERS, -PI, -PI2) ctx.set_source(tracing) ctx.stroke_preserve() ctx.set_source(filling) ctx.fill() ctx.save() ctx.set_source_rgb(0, 0, 0) ctx.move_to(BLOCK_INNER_PAD, BLOCK_INNER_PAD) ctx.show_text(action['name']) ctx.stroke() ctx.restore() ctx.translate(BLOCK_INNER_PAD, (height-SNAP_HEIGHT)/2) _render_snapshot(ctx, action) ctx.restore() def render_event(ctx, width, height, event): ctx.save() inner_width = width-(BLOCK_CORNERS<<1) inner_height = height-(BLOCK_CORNERS<<1) filling = cairo.LinearGradient(0, 0, 0, inner_height) if event not in selection: filling.add_color_stop_rgb(0.0, 1.0, 0.8, 0.6) filling.add_color_stop_rgb(1.0, 1.0, 0.6, 0.2) else: filling.add_color_stop_rgb(0.0, 0.8, 0.6, 0.4) filling.add_color_stop_rgb(1.0, 0.6, 0.4, 0.1) tracing = cairo.LinearGradient(0, 0, 0, inner_height) tracing.add_color_stop_rgb(0.0, 1.0, 1.0, 1.0) tracing.add_color_stop_rgb(1.0, 0.3, 0.3, 0.3) ctx.move_to(BLOCK_CORNERS, 0) ctx.rel_line_to(inner_width, 0) ctx.rel_line_to(BLOCK_CORNERS, BLOCK_CORNERS) ctx.rel_line_to(0, inner_height) ctx.rel_line_to(-BLOCK_CORNERS, BLOCK_CORNERS) ctx.rel_line_to(-inner_width, 0) ctx.rel_line_to(-BLOCK_CORNERS, -BLOCK_CORNERS) ctx.rel_line_to(0, -inner_height) ctx.close_path() ctx.set_source(tracing) ctx.stroke_preserve() ctx.set_source(filling) ctx.fill() ctx.set_source_rgb(0, 0, 0) ctx.move_to(BLOCK_INNER_PAD, BLOCK_INNER_PAD) ctx.show_text(event['name']) ctx.stroke() ctx.translate(BLOCK_INNER_PAD, (height-SNAP_HEIGHT)/2) _render_snapshot(ctx, event) ctx.restore() def on_viewer_expose(widget, evt): ctx = widget.window.cairo_create() global alloc alloc = widget.get_allocation() ctx.set_source_pixmap(widget.window, widget.allocation.x, widget.allocation.y) #draw no more than our expose event intersects our child region = gtk.gdk.region_rectangle(widget.allocation) r = gtk.gdk.region_rectangle(evt.area) region.intersect(r) ctx.region (region) ctx.clip() ctx.paint() ctx.translate(BLOCK_PADDING, BLOCK_PADDING) painter = _paint_state(ctx, states) while painter.next(): pass if click_pos and drag_pos: ctx.set_matrix(cairo.Matrix()) ctx.rectangle(click_pos[0], click_pos[1], \ drag_pos[0]-click_pos[0], drag_pos[1]-click_pos[1]) ctx.set_source_rgba(0, 0, 1, 0.5) ctx.fill_preserve() ctx.stroke() return False def _on_click(widget, evt): # the rendering pipeline will work out the click validation process global selection, click_pos, drag_pos drag_pos = None drag_pos = click_pos = evt.get_coords() widget.queue_draw() widget.drag_source_set(gtk.gdk.BUTTON1_MASK, (('view', gtk.TARGET_SAME_APP, 42),), gtk.gdk.ACTION_DEFAULT) widget.drag_source_set_icon_pixbuf(gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, 100, 200).get_from_drawable(widget.window, widget.window.get_colormap(), int(click_pos[0]), 0, 0,0, 100,200)) selection = [] def _on_drag(widget, evt): global selection, click_pos, drag_pos drag_pos = evt.get_coords() widget.queue_draw() #if __name__=='__main__': if True: win = gtk.Window(gtk.WINDOW_TOPLEVEL) win.set_size_request(1000, 200) win.connect("destroy", gtk.mainquit) canvas = gtk.DrawingArea() win.add(canvas) canvas.set_app_paintable(True) canvas.connect_after("expose-event", on_viewer_expose) canvas.add_events(gtk.gdk.BUTTON_PRESS_MASK|gtk.gdk.BUTTON_MOTION_MASK) canvas.connect('button-press-event', _on_click) canvas.connect('motion-notify-event', _on_drag) win.show_all() gtk.main()