From 1aa4f36c80ff11faf5562474f65cf466e480b210 Mon Sep 17 00:00:00 2001 From: Wade Brainerd Date: Tue, 01 Apr 2008 12:22:16 +0000 Subject: Initial commit of Colors! v3. --- (limited to 'colors.py') diff --git a/colors.py b/colors.py new file mode 100755 index 0000000..4666f6d --- /dev/null +++ b/colors.py @@ -0,0 +1,1539 @@ +# Copyright 2008 by Jens Andersson and Wade Brainerd. +# This file is part of Colors! XO. +# +# Colors 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. +# +# Colors 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 Colors. If not, see . +#!/usr/bin/env python +"""Colors! XO painting activity. Based on Colors! by Collecting Smiles.""" + +# Note- I had to rename the self.canvas object *self.easel* for now, because the stupid sugar.window.Window +# super class of Activity has its own "canvas" member. If anyone has a better name, feel free to change it. + +# Reference links: +# +# Colors! - http://wiki.laptop.org/go/Colors! +# PyGTK - http://www.pygtk.org/pygtk2reference/ +# Sugar - http://dev.laptop.org/~cscott/joyride-1477-api/ +# GStreamer - http://pygstdocs.berlios.de/pygst-reference/index.html +# Activities - http://wiki.laptop.org/go/Sugar_Activity_Tutorial +# Sharing - http://wiki.laptop.org/go/Shared_Sugar_Activities + +# Import standard Python modules. +import logging, os, math, time, copy, json +from gettext import gettext as _ + +# Import the C++ component of the activity. +from colorsc import * + +# Import PyGTK. +import gobject, pygtk, gtk, pango +# Needed to avoid thread crashes with GStreamer +gobject.threads_init() + +# Import DBUS and mesh networking modules. +import dbus, telepathy, telepathy.client +from dbus import Interface +from dbus.service import method, signal +from dbus.gobject_service import ExportedGObject +from sugar.presence.tubeconn import TubeConnection +from sugar.presence import presenceservice + +# Import Sugar UI modules. +from sugar import graphics +from sugar.activity import activity +from sugar.graphics import * +from sugar.graphics import toggletoolbutton + +# Import GStreamer (for camera access). +import pygst, gst + +# Initialize logging. +log = logging.getLogger('Colors run') +log.setLevel(logging.DEBUG) +logging.basicConfig() + +# Track memory leaks. +#import gc +#gc.set_debug(gc.DEBUG_LEAK) + +# DBUS identifiers are used to uniquely identify the activity for network communcations. +DBUS_IFACE = "org.laptop.community.Colors" +DBUS_PATH = "/org/laptop/community/Colors" +DBUS_SERVICE = DBUS_IFACE + +# This is the overlay that appears when the user presses the Palette toobar button. It covers the entire screen, +# and offers controls for brush type, size, opacity, and color. +# +# The color wheel and triangle are rendered into GdkImage objects by C++ code in palette.h / palette.cpp which +# is compiled into the colorsc module. +class BrushControlsPanel(gtk.HBox): + PALETTE_SIZE = 500 + PREVIEW_SIZE = 250 + BRUSHTYPE_SIZE = 100 + + def __init__ (self): + gtk.HBox.__init__(self) + self.set_property("spacing", 20) + self.set_border_width(50) + + # Locally managed Brush object. + self.brush = Brush() + + # Palette wheel widget. + palbox = gtk.VBox() + self.palette = Palette(BrushControlsPanel.PALETTE_SIZE) + self.paletteimage = gtk.gdk.Image(gtk.gdk.IMAGE_FASTEST, gtk.gdk.visual_get_system(), BrushControlsPanel.PALETTE_SIZE, BrushControlsPanel.PALETTE_SIZE) + self.palette.render_wheel(self.paletteimage) + self.palette.render_triangle(self.paletteimage) + self.palettearea = gtk.DrawingArea() + self.palettearea.set_size_request(BrushControlsPanel.PALETTE_SIZE, BrushControlsPanel.PALETTE_SIZE) + self.palettearea.add_events(gtk.gdk.POINTER_MOTION_MASK|gtk.gdk.BUTTON_PRESS_MASK|gtk.gdk.BUTTON_RELEASE_MASK) + self.palettearea.connect('expose-event', self.on_palette_expose) + self.palettearea.connect('motion-notify-event', self.on_palette_mouse) + self.palettearea.connect('button-press-event', self.on_palette_mouse) + self.palettearea.connect('button-release-event', self.on_palette_mouse) + palbox.pack_start(self.palettearea, False, False) + self.pack_start(palbox, False) + + # Brush size scrollbar, label and pressure sensitivity checkbox. + sizebox = gtk.VBox() + sizelabel = gtk.Label(_('Brush Size')) + sizebox.pack_end(sizelabel, False) + self.size = gtk.Adjustment(50, 1, 260, 1, 10, 10) + self.sizebar = gtk.VScale(self.size) + self.sizebar.set_property("draw-value", False) + self.sizebar.set_property("inverted", True) + self.sizebar.connect('value-changed', self.on_size_change) + sizebox.pack_end(self.sizebar) + self.sizecheck = gtk.CheckButton(_('Pressure\nSensitive')) + self.sizecheck.connect('toggled', self.on_variable_size_toggle) + sizebox.pack_end(self.sizecheck, False) + self.pack_start(sizebox, False) + + # Brush opacity scrollbar, label and pressure sensitivity checkbox. + opacitybox = gtk.VBox() + opacitylabel = gtk.Label(_('Brush Opacity')) + opacitybox.pack_end(opacitylabel, False) + self.opacity = gtk.Adjustment(0, 0, 1.1, 0.001, 0.1, 0.1) + self.opacitybar = gtk.VScale(self.opacity) + self.opacitybar.set_property("draw-value", False) + self.opacitybar.set_property("inverted", True) + self.opacitybar.connect('value-changed', self.on_opacity_change) + opacitybox.pack_end(self.opacitybar) + self.opacitycheck = gtk.CheckButton(_('Pressure\nSensitive')) + self.opacitycheck.connect('toggled', self.on_variable_opacity_toggle) + opacitybox.pack_end(self.opacitycheck, False) + self.pack_start(opacitybox, False) + + # Force column scrollbars to be equal width. + group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) + group.add_widget(sizebox) + group.add_widget(opacitybox) + + # Brush preview widget. + brushbox = gtk.VBox() + brushbox.set_property("spacing", 20) + + self.preview = BrushPreview(BrushControlsPanel.PREVIEW_SIZE) + self.previewimage = gtk.gdk.Image(gtk.gdk.IMAGE_FASTEST, gtk.gdk.visual_get_system(), BrushControlsPanel.PREVIEW_SIZE, BrushControlsPanel.PREVIEW_SIZE) + self.previewarea = gtk.DrawingArea() + self.previewarea.set_size_request(BrushControlsPanel.PREVIEW_SIZE, BrushControlsPanel.PREVIEW_SIZE) + self.previewarea.connect('expose-event', self.on_preview_expose) + brushbox.pack_start(self.previewarea, False) + + # Brush type selection widgets. + self.brushbtns = [] + brushtbl = gtk.Table(1, 2) + brushtbl.set_col_spacings(5) + brushtbl.attach(self.create_brushtype_widget(BrushType.BRUSHTYPE_SOFT), 0, 1, 0, 1) + brushtbl.attach(self.create_brushtype_widget(BrushType.BRUSHTYPE_HARD), 1, 2, 0, 1) + + brushbox.pack_start(brushtbl, False) + + self.pack_start(brushbox, False) + + self.in_toggle_cb = False + + def create_brushtype_widget (self, type): + brusharea = gtk.DrawingArea() + brusharea.set_size_request(BrushControlsPanel.BRUSHTYPE_SIZE, BrushControlsPanel.BRUSHTYPE_SIZE) + brusharea.connect('expose-event', self.on_brushtype_expose) + brusharea.preview = BrushPreview(BrushControlsPanel.BRUSHTYPE_SIZE) + brusharea.preview.brush.size = int(BrushControlsPanel.BRUSHTYPE_SIZE*0.75) + brusharea.preview.brush.type = type + brusharea.preview.brush.color = Color(0,0,0,0) + brusharea.previewimage = gtk.gdk.Image(gtk.gdk.IMAGE_FASTEST, gtk.gdk.visual_get_system(), BrushControlsPanel.BRUSHTYPE_SIZE, BrushControlsPanel.BRUSHTYPE_SIZE) + brusharea.preview.render(brusharea.previewimage) + brushbtn = gtk.ToggleButton() + brushbtn.set_image(brusharea) + brushbtn.connect('toggled', self.on_brushtype_toggle) + brushbtn.brushtype = type + self.brushbtns.append(brushbtn) + return brushbtn + + def set_brush (self, brush): + self.brush = brush + self.opacity.set_value(brush.opacity) + self.size.set_value(brush.size) + self.palette.set_color(brush.color) + self.opacitycheck.set_active((brush.control & Brush.BRUSHCONTROL_VARIABLEOPACITY) != 0) + self.sizecheck.set_active((brush.control & Brush.BRUSHCONTROL_VARIABLESIZE) != 0) + self.update_brushtype_btns() + + def on_size_change (self, event): + self.brush.size = int(self.size.get_value()) + self.previewarea.queue_draw() + + def on_opacity_change (self, event): + self.brush.opacity = self.opacity.get_value() + self.previewarea.queue_draw() + + def on_variable_size_toggle (self, event): + if self.sizecheck.get_active(): + self.brush.control |= Brush.BRUSHCONTROL_VARIABLESIZE + else: + self.brush.control &= ~Brush.BRUSHCONTROL_VARIABLESIZE + + def on_variable_opacity_toggle (self, event): + if self.opacitycheck.get_active(): + self.brush.control |= Brush.BRUSHCONTROL_VARIABLEOPACITY + else: + self.brush.control &= ~Brush.BRUSHCONTROL_VARIABLEOPACITY + + def on_palette_expose (self, widget, event): + self.cur_style = self.palettearea.get_style() + self.cur_gc = self.cur_style.fg_gc[gtk.STATE_NORMAL] + + # Draw palette image. + self.palette.render_triangle(self.paletteimage) + self.palettearea.window.draw_image(self.cur_gc, self.paletteimage, 0, 0, 0, 0, -1, -1) + + # Draw circles to indicate selected color. + # todo- Better looking circles. + r = int(self.palette.WHEEL_WIDTH*0.75) + + self.cur_gc.foreground = self.palettearea.get_colormap().alloc_color(16384,16384,16384) + self.cur_gc.line_width = 2 + + wheel_pos = self.palette.get_wheel_pos() + tri_pos = self.palette.get_triangle_pos() + + self.palettearea.window.draw_arc(self.cur_gc, False, int(wheel_pos.x-r/2+2), int(wheel_pos.y-r/2+2), r-4, r-4, 0, 360*64) + self.palettearea.window.draw_arc(self.cur_gc, False, int(tri_pos.x-r/2+2), int(tri_pos.y-r/2+2), r-4, r-4, 0, 360*64) + + self.cur_gc.foreground = self.palettearea.get_colormap().alloc_color(65535,65535,65535) + self.cur_gc.line_width = 2 + + self.palettearea.window.draw_arc(self.cur_gc, False, int(wheel_pos.x-r/2), int(wheel_pos.y-r/2), r, r, 0, 360*64) + self.palettearea.window.draw_arc(self.cur_gc, False, int(tri_pos.x-r/2), int(tri_pos.y-r/2), r, r, 0, 360*64) + + def on_palette_mouse (self, widget, event): + if event.state & gtk.gdk.BUTTON1_MASK: + widget.grab_focus() + self.palette.process_mouse(int(event.x), int(event.y)) + self.palettearea.queue_draw() + self.brush.color = self.palette.get_color() + self.previewarea.queue_draw() + if event.type == gtk.gdk.BUTTON_RELEASE: + self.palette.process_mouse_release() + + def on_preview_expose (self, widget, event): + self.preview.brush = self.brush + self.preview.brush.size = self.brush.size*2 # Mimic 2x canvas scaling. + self.preview.render(self.previewimage) + self.previewarea.window.draw_image(widget.get_style().fg_gc[gtk.STATE_NORMAL], self.previewimage, 0, 0, 0, 0, -1, -1) + + def on_brushtype_expose (self, widget, event): + widget.window.draw_image(widget.get_style().fg_gc[gtk.STATE_NORMAL], widget.previewimage, 0, 0, 0, 0, -1, -1) + + # Manually implemented radio button using ToggleButtons. + def update_brushtype_btns (self): + for b in self.brushbtns: + b.set_active(b.brushtype == self.brush.type) + + def on_brushtype_toggle (self, widget): + if self.in_toggle_cb: + return + self.in_toggle_cb = True + + self.brush.type = widget.brushtype + self.update_brushtype_btns() + self.previewarea.queue_draw() + + self.in_toggle_cb = False + +# This is the overlay that appears when playing back large numbers of drawing commands. +# It simply shows how much work is left to do and that progress is taking place. +class ProgressPanel(gtk.VBox): + def __init__ (self): + gtk.VBox.__init__(self) + self.set_border_width(50) + + self.label = gtk.Label() + self.label.set_markup(""+_("Working...")+"") + self.progress = gtk.ProgressBar() + self.progress.set_fraction(0.5) + self.progress.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT) + + vbox = gtk.VBox() + vbox.set_property("spacing", 20) + + vbox.pack_start(self.label, False) + vbox.pack_start(self.progress, False) + + self.pack_start(vbox, True, False) + +# This is the main Colors! activity class. +# +# It owns the main application window, the painting canvas, and all the various toolbars and options. +class Colors(activity.Activity, ExportedGObject): + # Application mode definitions. + MODE_INTRO = 0 + MODE_PLAYBACK = 1 + MODE_CANVAS = 2 + MODE_SCROLL = 3 + MODE_PALETTE = 4 + MODE_REFERENCE = 5 + + # Button definitions + BUTTON_PALETTE = 1<<0 + BUTTON_REFERENCE = 1<<1 + BUTTON_VIDEOPAINT = 1<<2 + BUTTON_SCROLL = 1<<3 + BUTTON_PICK = 1<<4 + BUTTON_ZOOM_IN = 1<<5 + BUTTON_ZOOM_OUT = 1<<6 + BUTTON_SIZE_0 = 1<<7 + BUTTON_SIZE_1 = 1<<8 + BUTTON_SIZE_2 = 1<<9 + BUTTON_SIZE_3 = 1<<10 + BUTTON_TOUCH = 1<<11 + + # Number of drawing steps to execute between progress bar updates. More updates means faster overall drawing + # but a less responsive UI. + PROGRESS_DELTA = 50 + + def __init__ (self, handle): + activity.Activity.__init__(self, handle) + self.set_title(_("Colors!")) + + # Uncomment to test out a bunch of the C++ heavy lifting APIs. Takes awhile on the XO though. + #self.benchmark() + + # Get activity size. What we really need is the size of the canvasarea, not including the toolbox. + # This will be figured out on the first paint event, once everything is resized. + self.width = gtk.gdk.screen_width() + self.height = gtk.gdk.screen_height() + + # Set up various systems. + self.init_input() + self.init_zoom() + self.init_scroll() + + # Build the toolbar. + self.build_toolbar() + + # Set up drawing canvas (which is also the parent for any popup widgets like the brush controls). + self.build_canvas() + + # Build the brush control popup window. + self.build_brush_controls() + + # Build the progress display popup window. + self.build_progress() + + # Start camera processing. + self.init_camera() + + # Set the initial mode to None, it will be set to Intro on the first update. + self.mode = None + + # Set up mesh networking. + self.init_mesh() + + # Start the framerate timer. + gobject.idle_add(self.tick) + + # This has to happen last, because it calls the read_file method when restoring from the Journal. + self.set_canvas(self.easelarea) + + # Reveal the main window (but not the panels). + self.show_all() + self.brush_controls.hide() + self.progress.hide() + self.overlay_active = False + + #----------------------------------------------------------------------------------------------------------------- + # User interface construction + + def build_canvas (self): + # The canvasarea is the main window which covers the entire screen below the toolbar. + self.easelarea = gtk.Layout() + self.easelarea.set_size_request(gtk.gdk.screen_width(), gtk.gdk.screen_height()) + self.easelarea.set_flags(gtk.CAN_FOCUS) + + self.set_double_buffered(False) + self.easelarea.set_double_buffered(False) + + # Set up GTK events for the canvasarea. + self.easelarea.add_events(gtk.gdk.POINTER_MOTION_MASK|gtk.gdk.POINTER_MOTION_HINT_MASK) + self.easelarea.add_events(gtk.gdk.BUTTON_PRESS_MASK|gtk.gdk.BUTTON_RELEASE_MASK) + self.easelarea.add_events(gtk.gdk.KEY_PRESS_MASK|gtk.gdk.KEY_RELEASE_MASK) + + # The actual drawing canvas is at 1/2 resolution, which improves performance by 4x and still leaves a decent + # painting resolution of 600x400 on the XO. + self.easel = Canvas(self.width/2, self.height/2) + self.set_brush(self.easel.brush) + + # The Canvas internally stores the image as 32bit. When rendering, it scales up and blits into canvasimage, + # which is in the native 565 resolution of the XO. Then canvasimage is drawn into the canvasarea DrawingArea. + self.easelimage = gtk.gdk.Image(gtk.gdk.IMAGE_FASTEST, gtk.gdk.visual_get_system(), self.width, self.height) + + # Now that we have a canvas, connect the rest of the events. + self.easelarea.connect('expose-event', self.on_canvasarea_expose) + self.easelarea.connect('expose-event', self.on_canvasarea_expose) + self.easelarea.connect('key-press-event', self.on_key_event) + self.easelarea.connect('key-release-event', self.on_key_event) + self.easelarea.connect('button-press-event', self.on_mouse_button) + self.easelarea.connect('button-release-event', self.on_mouse_button) + self.easelarea.connect('motion-notify-event', self.on_mouse_motion) + + def build_brush_controls (self): + self.brush_controls = BrushControlsPanel() + self.brush_controls.set_size_request(gtk.gdk.screen_width(), gtk.gdk.screen_height()) + self.easelarea.put(self.brush_controls, 0, 0) + + def build_progress (self): + self.progress = ProgressPanel() + self.progress.set_size_request(gtk.gdk.screen_width(), gtk.gdk.screen_height()) + self.easelarea.put(self.progress, 0, 0) + + def build_toolbar (self): + # Painting controls (palette, zoom, etc) + self.palettebtn = toggletoolbutton.ToggleToolButton('palette') + self.palettebtn.set_tooltip(_("Palette")) + self.palettebtn.connect('clicked', self.on_palette) + self.palette_forced = False + + # todo- Color picker button, similar semantics to palette button. + + #self.zoomoutbtn = toolbutton.ToolButton('zoom-out') + #self.zoomoutbtn.set_tooltip(_("Zoom Out")) + #self.zoominbtn = toolbutton.ToolButton('zoom-in') + #self.zoominbtn.set_tooltip(_("Zoom In")) + + self.refsep = gtk.SeparatorToolItem() + + self.takerefbtn = toolbutton.ToolButton('reference') + self.takerefbtn.set_tooltip(_("Take Reference Picture")) + self.takerefbtn.connect('clicked', self.on_take_reference) + self.take_reference = False + + self.showrefbtn = toggletoolbutton.ToggleToolButton('zoom-out') + self.showrefbtn.set_tooltip(_("Show Reference Picture")) + self.showrefbtn.connect('clicked', self.on_show_reference) + self.reference_forced = False + + self.videopaintsep = gtk.SeparatorToolItem() + + self.videopaintbtn = toggletoolbutton.ToggleToolButton('zoom-out') + self.videopaintbtn.set_tooltip(_("Video Paint")) + self.videopaintbtn.connect('clicked', self.on_videopaint) + self.videopaintpreview = gtk.DrawingArea() + self.videopaintpreview.set_size_request(Canvas.VIDEO_WIDTH, Canvas.VIDEO_HEIGHT) + self.videopaintpreview.connect('expose-event', self.on_videopaintpreview_expose) + self.videopaintitem = gtk.ToolItem() + self.videopaintitem.add(self.videopaintpreview) + self.videopaintimage = gtk.gdk.Image(gtk.gdk.IMAGE_FASTEST, gtk.gdk.visual_get_system(), Canvas.VIDEO_WIDTH, Canvas.VIDEO_HEIGHT) + self.videopaint_enabled = False + + self.clearsep = gtk.SeparatorToolItem() + self.clearsep.set_expand(True) + self.clearsep.set_draw(False) + + self.clearbtn = toolbutton.ToolButton('erase') + self.clearbtn.set_tooltip(_("Start Over")) + self.clearbtn.connect('clicked', self.on_clear) + + paintbox = gtk.Toolbar() + paintbox.insert(self.palettebtn, -1) + #paintbox.insert(self.zoomoutbtn, -1) + #paintbox.insert(self.zoominbtn, -1) + paintbox.insert(self.refsep, -1) + paintbox.insert(self.takerefbtn, -1) + paintbox.insert(self.showrefbtn, -1) + paintbox.insert(self.videopaintsep, -1) + paintbox.insert(self.videopaintbtn, -1) + paintbox.insert(self.videopaintitem, -1) + paintbox.insert(self.clearsep, -1) + paintbox.insert(self.clearbtn, -1) + + # Playback controls + self.startbtn = toolbutton.ToolButton('media-playback-start') + self.startbtn.set_tooltip(_("Start Playback")) + self.startbtn.connect('clicked', self.on_play) + + self.pausebtn = toolbutton.ToolButton('media-playback-pause') + self.pausebtn.set_tooltip(_("Pause Playback")) + self.pausebtn.connect('clicked', self.on_pause) + + self.beginbtn = toolbutton.ToolButton('media-seek-backward') + self.beginbtn.set_tooltip(_("Skip To Beginning")) + self.beginbtn.connect('clicked', self.on_skip_begin) + + self.endbtn = toolbutton.ToolButton('media-seek-forward') + self.endbtn.connect('clicked', self.on_skip_end) + self.endbtn.set_tooltip(_("Skip To End")) + + # Position bar + self.playbackpossep = gtk.SeparatorToolItem() + self.playbackpossep.set_draw(True) + + self.playbackpos = gtk.Adjustment(0, 0, 110, 1, 10, 10) + self.playbackposbar = gtk.HScale(self.playbackpos) + self.playbackposbar.connect('value-changed', self.on_playbackposbar_change) + self.playbackposbar.ignore_change = 0 + + self.playbackpositem = gtk.ToolItem() + self.playbackpositem.set_expand(True) + self.playbackpositem.add(self.playbackposbar) + + playbox = gtk.Toolbar() + playbox.insert(self.startbtn, -1) + playbox.insert(self.pausebtn, -1) + playbox.insert(self.beginbtn, -1) + playbox.insert(self.endbtn, -1) + playbox.insert(self.playbackpossep, -1) + playbox.insert(self.playbackpositem, -1) + + # Sample files to learn from. Reads the list from an INDEX file in the data folder. + samplebox = gtk.Toolbar() + self.samplebtns = [] + + samples = [] + fd = open(activity.get_bundle_path() + '/data/INDEX', 'r') + try: + samples = json.read(fd.read()) + finally: + fd.close() + + log.debug("Samples: %r", samples) + for s in samples: + btn = toolbutton.ToolButton('media-playback-start') + btn.filename = activity.get_bundle_path() + '/data/' + s['drw'] + btn.set_tooltip(s['title']) + img = gtk.Image() + img.set_from_file(activity.get_bundle_path() + '/data/' + s['icon']) + btn.set_icon_widget(img) + btn.connect('clicked', self.on_sample) + samplebox.insert(btn, -1) + self.samplebtns.append(btn) + + toolbar = activity.ActivityToolbox(self) + toolbar.add_toolbar(_("Paint"),paintbox) + toolbar.add_toolbar(_("Watch"),playbox) + toolbar.add_toolbar(_("Learn"),samplebox) + toolbar.show_all() + self.set_toolbox(toolbar) + + #----------------------------------------------------------------------------------------------------------------- + # Camera access + # + # todo- Consider going straight to video4linux2's C API, we aren't really using any features of GStreamer. + + def init_camera (self): + self.gstpipe = gst.parse_launch("v4l2src ! fakesink name=fakesink signal-handoffs=true") + self.gstfakesink = self.gstpipe.get_by_name('fakesink') + self.gstfakesink.connect("handoff", self.on_gst_buffer) + self.easelarea.connect("destroy", self.on_gst_destroy) + + self.gstpipe.set_state(gst.STATE_PLAYING) + + def on_gst_buffer(self, element, buffer, pad): + if self.take_reference: + self.easel.set_reference_buffer(buffer, 640, 480) + # Bring up the reference screen and update. + self.showrefbtn.set_active(True) + self.easel.render_reference_overlay() + self.flush_entire_canvas() + self.take_reference = False + + if self.videopaint_enabled: + self.easel.videopaint_motion(buffer, 640, 480) + + # todo- This isn't working right, for some reason the GST pipe keeps playing after the window is destroyed, leading + # to Python exceptions in on_gst_buffer. + def on_gst_destroy (self, widget): + self.gstpipe.set_state(gst.STATE_NULL) + + #----------------------------------------------------------------------------------------------------------------- + # Mesh networking + # + # The mesh networking system is a little bit wacky, but works reasonably well at the moment. It might need to + # be redone in the future for less stable networking environments. + # + # Each user maintains the current state of the canvas, as well as a 'shared image' state which represents the + # 'master' state of the canvas that is shared by all the users. The command index in the drawing command list + # that corresponds to the master state is also recorded. + # + # Each user is allowed to paint 'ahead' of the master state, by appending commands to their local command list. + # After each stroke, the commands of the stroke are broadcast as the new master state to the other users. + # + # Whenever a new master state is received, it will contain a list of commands that follow the old master state + # to reach the new one. The user simply rewinds their canvas to the old master state, replaces their local image + # with the shared one, and appends the received commands to reach the new master state. + # + # The net effect is that when the user paints something, they broadcast their own commands, and then *receive* + # their own commands, rewinding and then playing them back immediately. So, every users canvas state is simply the + # sum of all the broadcasts from themselves and the other users. Since the broadcasts are serialized, every + # user has the same state all the time. + + def init_mesh (self): + self.connected = False # If True, the activity is shared with other users. + self.initiating = False # If True, this instance started the activity. Otherwise, we joined it. + + # Set up the drawing send and receive state. + # See send_and_receive_draw_commands for more information. + self.draw_command_sent = 0 + self.draw_command_received = 0 + self.draw_command_queue = DrawCommandBuffer() + + # Get the presence server and self handle. + self.pservice = presenceservice.get_instance() + self.owner = self.pservice.get_owner() + + self.connect('shared', self.on_shared) # Called when the user clicks the Share button in the toolbar. + self.connect('joined', self.on_join) # Called when the activity joins a remote activity. + + def on_shared (self, activity): + self.initiating = True + self.setup_sharing() + + # Offer a DBus tube that everyone else can connect to. + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube(DBUS_SERVICE, {}) + + # Cancel the intro if playing. + self.set_mode(Colors.MODE_CANVAS) + + def on_list_tubes_reply(self, tubes): # Called by on_join. + for tube_info in tubes: + self.on_tube(*tube_info) + + def on_list_tubes_error(self, e): # Called by on_join. + pass + + def on_join (self, activity): + self.initiating = False + self.setup_sharing() + + # List existing tubes. There should only be one, which will invoke the on_new_tube callback. + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes( + reply_handler=self.on_list_tubes_reply, error_handler=self.on_list_tubes_error) + + # Limit UI choices for non-host users. + self.disable_shared_commands() + + # Cancel the intro if playing. + self.set_mode(Colors.MODE_CANVAS) + + def setup_sharing (self): + """Called to initialize mesh networking objects when the activity becomes shared (on_shared) or joins an + existing shared activity (on_join).""" + # Cache connection related objects. + self.conn = self._shared_activity.telepathy_conn + self.tubes_chan = self._shared_activity.telepathy_tubes_chan + self.text_chan = self._shared_activity.telepathy_text_chan + + # This will get called as soon as the connection is established. + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube', self.on_tube) + + # Called when a buddy joins us or leaves (does nothing right now). + self._shared_activity.connect('buddy-joined', self.on_buddy_joined) + self._shared_activity.connect('buddy-left', self.on_buddy_left) + + def on_tube (self, id, initiator, type, service, params, state): + """Called by the NewTube callback or the ListTubes enumeration, when a real connection finally exists.""" + if (type == telepathy.TUBE_TYPE_DBUS and service == DBUS_SERVICE): + # If the new tube is waiting for us to finalize it, do so. + if state == telepathy.TUBE_STATE_LOCAL_PENDING: + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].AcceptDBusTube(id) + + if not self.connected: + # Create the TubeConnection object to manage the connection. + self.tube = TubeConnection(self.conn, + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES], + id, group_iface=self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP]) + ExportedGObject.__init__(self, self.tube, DBUS_PATH) + + # Set up DBUS Signal receiviers. + self.tube.add_signal_receiver(self.ReceiveHello, 'BroadcastHello', DBUS_IFACE, path=DBUS_PATH) + self.tube.add_signal_receiver(self.ReceiveCanvasMode, 'BroadcastCanvasMode', DBUS_IFACE, path=DBUS_PATH) + self.tube.add_signal_receiver(self.ReceiveClear, 'BroadcastClear', DBUS_IFACE, path=DBUS_PATH) + self.tube.add_signal_receiver(self.ReceiveDrawCommands, 'BroadcastDrawCommands', DBUS_IFACE, path=DBUS_PATH) + self.tube.add_signal_receiver(self.ReceivePlayback, 'BroadcastPlayback', DBUS_IFACE, path=DBUS_PATH) + + log.debug("Connected.") + self.connected = True + + # Announce our presence to the server. + if not self.initiating: + self.BroadcastHello() + + # Notes about DBUS signals: + # - When you call a @signal function, its implementation is invoked, and the registered callback is invoked on all + # the peers (including the one who invoked the signal!). So it's usually best for the @signal function to do + # nothing at all. + # - The 'signature' describes the parameters to the function. + + @signal(dbus_interface=DBUS_IFACE, signature='') + def BroadcastHello (self): + """Broadcast signal sent when a client joins the shared activity.""" + pass + def ReceiveHello (self): + if not self.initiating: return # Only the initiating peer responds to Hello commands. + log.debug("Received Hello. Responding with canvas state (%d commands).", self.easel.playback_length()) + self.BroadcastCanvasMode() + self.BroadcastClear() + buf = self.easel.send_drw_commands(0, self.easel.get_num_commands()) + self.BroadcastDrawCommands(buf.get_bytes(), buf.ncommands) + + @signal(dbus_interface=DBUS_IFACE, signature='') + def BroadcastCanvasMode (self): + """Broadcast signal for forcing clients into Canvas mode.""" + pass + def ReceiveCanvasMode (self): + log.debug("ReceiveCanvasMode") + if self.mode != Colors.MODE_CANVAS: + self.set_mode(Colors.MODE_CANVAS) + + @signal(dbus_interface=DBUS_IFACE, signature='') + def BroadcastClear (self): + """Broadcast signal for clearing the canvas.""" + pass + def ReceiveClear (self): + log.debug("ReceiveClear") + self.easel.clear() + self.easel.save_shared_image() + + @signal(dbus_interface=DBUS_IFACE, signature='ayi') + def BroadcastDrawCommands (self, cmds, ncommands): + """Broadcast signal for drawing commands.""" + pass + def ReceiveDrawCommands (self, cmds, ncommands): + log.debug("ReceiveDrawCommands") + s = "".join(chr(b) for b in cmds) # Convert dbus.ByteArray to Python string. + self.draw_command_queue.append(DrawCommandBuffer(s, ncommands)) + + @signal(dbus_interface=DBUS_IFACE, signature='bii') + def BroadcastPlayback (self, playing, playback_pos, playback_speed): + """Broadcast signal controlling playback. Not yet used.""" + pass + def ReceivePlayback (self): + log.debug("ReceivePlayback") + if playing: + if self.mode != Colors.MODE_PLAYBACK: + self.set_mode(Colors.MODE_PLAYBACK) + else: + if self.mode == Colors.MODE_PLAYBACK: + self.set_mode(Colors.MODE_CANVAS) + self.easel.playback_to(playback_pos) + self.easel.set_playback_speed(playback_speed) + + def on_buddy_joined (self, activity, buddy): + log.debug('Buddy %s joined', buddy.props.nick) + + def on_buddy_left (self, activity, buddy): + log.debug('Buddy %s left', buddy.props.nick) + + def send_and_receive_draw_commands (self): + if self.connected: + # Broadcast drawing commands that were generated by this user since the last call to this function. + if self.draw_command_sent < self.easel.get_num_commands(): + buf = self.easel.send_drw_commands(self.draw_command_sent, self.easel.get_num_commands()-self.draw_command_sent) + self.BroadcastDrawCommands(buf.get_bytes(), buf.ncommands) + + # Play any queued draw commands that were received from the host. If there are any, we first reset the + # canvas contents back to the last received state and then play them back. + if self.draw_command_queue.ncommands: + # Also, we have to save and restore the brush around the queued commands. + saved_brush = self.easel.brush + self.easel.receive_drw_commands(self.draw_command_queue, self.draw_command_sent) + self.easel.restore_shared_image() + self.easel.play_range(self.draw_command_sent, self.easel.get_num_commands()) + self.easel.save_shared_image() + self.draw_command_queue.clear() + self.set_brush(saved_brush) + self.flush_dirty_canvas() + + # Note that resetting the state above means "undoing" the commands we just broadcast. We will receive them + # again by our ReceiveDrawCommands callback immediately, and will play them back so the user shouldn't notice. + self.draw_command_sent = self.easel.get_num_commands() + + def disable_shared_commands (self): + """Disables UI controls which cannot be activated by non-host peers.""" + # Cannot clear the canvas. + self.clearbtn.set_sensitive(False) + + # Cannot control playback. + self.startbtn.set_sensitive(False) + self.pausebtn.set_sensitive(False) + self.beginbtn.set_sensitive(False) + self.endbtn.set_sensitive(False) + self.playbackposbar.set_sensitive(False) + + # Cannot activate sample drawings. + for s in self.samplebtns: + s.set_sensitive(False) + + #----------------------------------------------------------------------------------------------------------------- + # Input code + + def init_input (self): + self.cur_buttons = 0 + self.pending_press = 0 + self.pending_release = 0 + self.mx = 0 + self.my = 0 + self.pressure = 255 + self.lastmx = 0 + self.lastmy = 0 + self.lastr = 0 + + def on_key_event (self, widget, event): + log.debug("on_key_event: %d", event.keyval) # Useful for manually working out keyvals for OLPC keys. + + # OLPC keymap is designed to allow left or right handed stylus use, with lots of redundancy. + # So each major key should appear at least once on each side of the keyboard. + button = 0 + + # 'p' for Palette (todo- need something better!). + if event.keyval == ord('p'): + button = Colors.BUTTON_PALETTE + + # 'r' for Reference (todo- need something better!). + if event.keyval == ord('r'): + button = Colors.BUTTON_REFERENCE + + # 'v' for Videopaint (todo- need something better!). + if event.keyval == ord('v'): + button = Colors.BUTTON_VIDEOPAINT + + # 's' hotkey to save PNG thumbnail of the current canvas as 'thumb.png'. + if event.keyval == ord('s'): + self.save_thumbnail(activity.get_bundle_path() + '/thumb.png') + + # OLPC 'hand' buttons for scrolling. + if event.keyval == 311 or event.keyval == 312 or event.keyval == ord('s'): + button = Colors.BUTTON_SCROLL + + # OLPC 'size' buttons for intensity. + #if event.keyval == 286: button = Colors.BUTTON_SIZE_0 + #if event.keyval == 287: button = Colors.BUTTON_SIZE_1 + #if event.keyval == 288: button = Colors.BUTTON_SIZE_2 + #if event.keyval == 289: button = Colors.BUTTON_SIZE_3 + + # Arrow keys, gamepad 'face' buttons, or 'z' and 'x' for Zoom. + if event.keyval == ord('z') or event.keyval == 264 or event.keyval == 265 or event.keyval == 273: + button = Colors.BUTTON_ZOOM_IN + if event.keyval == ord('x') or event.keyval == 258 or event.keyval == 259 or event.keyval == 274: + button = Colors.BUTTON_ZOOM_OUT + + # Either Alt key for pick. + if event.keyval == 313 or event.keyval == 308: + button = Colors.BUTTON_PICK + + if button != 0: + if event.type == gtk.gdk.KEY_PRESS: + self.pending_press = self.pending_press | button + else: + self.pending_release = self.pending_release | button + + def on_mouse_button(self, widget, event): + if self.overlay_active: + return + if event.button == 1: + if event.type == gtk.gdk.BUTTON_PRESS: + self.pending_press = self.pending_press | Colors.BUTTON_TOUCH + if event.type == gtk.gdk.BUTTON_RELEASE: + self.pending_release = self.pending_release | Colors.BUTTON_TOUCH + #else: + # if event.type == gtk.gdk.BUTTON_PRESS: + # self.pending_press = self.pending_press | Colors.BUTTON_PALETTE + # if event.type == gtk.gdk.BUTTON_RELEASE: + # self.pending_release = self.pending_release | Colors.BUTTON_PALETTE + if not widget.is_focus(): + widget.grab_focus() + self.update() + return True + + def on_mouse_motion (self, widget, event): + if self.overlay_active: + return + # Process mouse event normally. + if event.is_hint: + x, y, state = event.window.get_pointer() + else: + x = event.x + y = event.y + state = event.state + self.mx = int(x) + self.my = int(y) + if not widget.is_focus(): + widget.grab_focus() + self.update() + self.flush_cursor() + return True + + def update_input (self): + buttons = self.cur_buttons + + buttons = buttons | self.pending_press + self.pending_press = 0 + + buttons = buttons & ~self.pending_release + self.pending_release = 0 + + hold = buttons & self.cur_buttons + + if self.cur_buttons != buttons: + changed = self.cur_buttons ^ buttons + press = changed & buttons + release = changed & self.cur_buttons + self.cur_buttons = buttons + if press != 0: + self.on_press(press) + if release != 0: + self.on_release(release) + + if hold != 0: + self.on_hold(hold) + + def on_press (self, button): + if button & Colors.BUTTON_ZOOM_IN: + self.zoom_in() + + if button & Colors.BUTTON_ZOOM_OUT: + self.zoom_out() + + if button & Colors.BUTTON_VIDEOPAINT: + self.videopaintbtn.set_active(not self.videopaint_enabled) + + def on_release (self, button): + pass + + def on_hold (self, button): + pass + + #----------------------------------------------------------------------------------------------------------------- + # Scroll code + # + # todo- Scrolling is currently broken. It needs to be bound to a key and the issues need to be fixed. + + def init_scroll (self): + self.scroll = Pos(0,0) + self.scrollref = None + + def scroll_to (self, pos): + self.scroll = pos + + # Clamp scroll position to within absolute limits. + scroll.x = min(max(scroll.x, -self.easel.width * 0.125), self.easel.width * 0.125) + scroll.y = min(max(scroll.y, -self.easel.height * 0.125), self.easel.height * 0.125) + + def snap_scroll (self): + """Animates the scroll position back towards reasonable bounds.""" + if self.scroll.x < self.easel.width *-0.05: self.scroll.x = self.scroll.x * 0.9 + if self.scroll.y < self.easel.height*-0.05: self.scroll.y = self.scroll.y * 0.9 + if self.scroll.x+self.width > self.easel.width*1.05: + self.scroll.x = (self.easel.width - self.width)*0.5 + self.scroll.x*0.5 + if self.scroll.y+self.height > self.easel.height*1.05: + self.scroll.y = (self.easel.height - self.height)*0.5 + self.scroll.x*0.5 + + #----------------------------------------------------------------------------------------------------------------- + # Zoom code + # + # todo- Zooming is currently broken. It needs to be bound to a key and the issues need to be fixed. + # + # The main issue is probably that the underlying canvas doesn't support enough scaled blit options and also + # doesn't support clipping. + + def init_zoom (self): + self.zoom = 1.0 + self.zoomref = None + + def zoom_to (self, zoom): + # End any current stroke. + self.end_draw() + + # Adjust scroll position to keep the same point centered on screen while the zoom changes. + # This is either the reference point (the center of the last stroke) or else the screen center. + #if self.zoomref != None: + # scrollcenter = self.zoomref + #else: + scrollcenter = self.scroll + Pos(self.width*0.5/self.zoom, self.height*0.5/self.zoom) + self.zoom = zoom + self.scroll_to(scrollcenter - Pos(self.width*0.5/self.zoom, self.height*0.5/self.zoom)) + self.zoomref = None + + def zoom_in (self): + if self.zoom == 0.75: + self.zoom_to(1.0) + self.scroll_to(Pos(0,0)) + elif self.zoom == 1.0: + self.zoom_to(2.0) + + def zoom_out (self): + if self.zoom == 2.0: + self.zoom_to(1.0) + self.scroll_to(Pos(0,0)) + elif self.zoom == 1.0: + self.zoom_to(0.75) + + #----------------------------------------------------------------------------------------------------------------- + # Drawing commands + # + # These commands instruct the underlying canvas to execute commands which modify the canvas. + # + # All drawing operations become commands which are executed immediately and also recorded so that the painting can + # be played back. + + def draw (self, pos): + relpos = pos * Pos(self.easel.width, self.easel.height) / Pos(self.zoom, self.zoom) + self.scroll + relpos = relpos / Pos(self.easel.width, self.easel.height) + self.easel.play_command(DrawCommand.create_draw(relpos, int(self.pressure)), True) + + def end_draw (self): + if self.easel.stroke: + self.easel.play_command(DrawCommand.create_end_draw(int(self.pressure)), True) + + # Send to sharing participants. + self.send_and_receive_draw_commands() + + # Record a new default zoom-in focal point. + #self.zoomref = (self.easel.strokemin + self.easel.strokemax) * Pos(0.5,0.5) + self.zoomref = Pos(self.mx, self.my) + + def set_brush (self, brush): + self.easel.play_command(DrawCommand.create_color_change(brush.color), True) + self.easel.play_command(DrawCommand.create_size_change(brush.control, brush.type, brush.size/float(self.easel.width), brush.opacity), True) + + def play_to_playbackpos (self): + """Plays drawing commands until playbackpos.get_value() is reached. This may involve resetting the canvas if + the given position is before the current position, since it is impossible to play commands backwards. + Called to skip the playback position, for example when fast forwarding or rewinding or dragging the scrollbar.""" + self.playbackposbar.ignore_change += 1 + + total_left = 0 + + # This used to be in the while True: loop, such that the overlay would only appear when + # there is a lot of work to do. It might go back there later. + self.progress.set_size_request(self.width, self.height) + self.progress.show_all() + self.easelarea.set_double_buffered(True) + self.overlay_active = True + self.flush_entire_canvas() + # Display and run the GTK loop. This hack takes a set number of events per loop, + # as checking the gtk.events_pending() function leads to an infinite loop. + for i in range(0, 5): + if gtk.main_iteration(False): + return + + # Keep looping until the position is reached. Since we activate the GTK event loop processing from within + # our inner loop, the user can actually move the scrollbar while this function is running! + while True: + to = int(self.playbackpos.get_value()/100.0*self.easel.playback_length()) + if self.easel.playback_pos() == to: + break + + # Rewind if needed. + if self.easel.playback_pos() > to: + self.easel.clear_image() + self.easel.start_playback() + + total_left = max(total_left, to - self.easel.playback_pos()) + + # Advance playback by as much as we can in 1/10th of a second. + startpos = self.easel.playback_pos() + starttk = time.time() + endtk = starttk + 1.0/10.0 + while self.easel.playback_pos() < to: + self.easel.playback_step_to(to) + if time.time() >= endtk: + break + #log.debug("from %d to %d in %fs", startpos, self.easel.playback_pos(), time.time()-starttk) + + # This is currently disabled as it leads to the playback position passing the requested position, which + # can cause infinite "Working..." loops when the mouse is held down. + # Leaving it out can lead to inaccurate playback, when the play-to position is mid-stroke. + #self.easel.playback_finish_stroke() + + # Update the progress bar. + if total_left > 0: + f = 1.0-float(to - self.easel.playback_pos())/total_left + self.progress.progress.set_fraction(f) + + # Comment this in to watch the painting and slow things down. + #self.flush_entire_canvas() + + # Display and run the GTK loop. This hack takes a set number of events per loop, + # as checking the gtk.events_pending() function leads to an infinite loop. + for i in range(0, 5): + if gtk.main_iteration(False): + return + + self.overlay_active = False + self.progress.hide_all() + self.easelarea.set_double_buffered(False) + + self.flush_entire_canvas() + self.playbackposbar.ignore_change -= 1 + + # Canvas repainting. These methods draw portions of the canvas to the canvasarea. + def flush_dirty_canvas (self): + """Causes a redraw of the canvas area which has been modified since the last call to this function.""" + self.easelarea.queue_draw_area( + int(self.easel.dirtymin.x*2), int(self.easel.dirtymin.y*2), + int(self.easel.dirtymax.x*2-self.easel.dirtymin.x*2+1), + int(self.easel.dirtymax.y*2-self.easel.dirtymin.y*2+1)) + self.easel.reset_dirty_rect() + + def flush_entire_canvas (self): + """Causes a redraw of the entire canvas area.""" + self.easelarea.queue_draw() + + def flush_cursor (self): + """Causes a redraw of the canvas area covered by the cursor.""" + r = int(self.easel.brush.size*2*self.pressure/256) + x0 = min(self.lastmx-self.lastr/2, self.mx-r/2) + y0 = min(self.lastmy-self.lastr/2, self.my-r/2) + x1 = max(self.lastmx+self.lastr/2, self.mx+r/2) + y1 = max(self.lastmy+self.lastr/2, self.my+r/2) + self.easelarea.queue_draw_area(x0, y0, x1-x0+2, y1-y0+2) + + #----------------------------------------------------------------------------------------------------------------- + # Application states + # + # Colors is controlled by a finite state machine which keeps track of the current application mode (painting, + # playing back, scrolling, zooming, etc) and manages transitions between states. + # + # Each state is allowed to process code when it is entered or left, for the purpose of initializing variables and + # cleaning up afterwards. + # + # todo- Consider breaking up into enter_intro, enter_playback, enter_canvas, etc. + + def enter_mode (self): + if self.mode == Colors.MODE_INTRO: + # Load and play intro movie. It was created on a DS at 60hz, so we need to speed it up drastically to + # make it watchable. + self.easel.clear() + self.easel.load(activity.get_bundle_path() + "/data/intro.drw") + self.easel.set_playback_speed(8) + self.easel.start_playback() + + if self.mode == Colors.MODE_PLAYBACK: + self.easel.set_playback_speed(1) + self.easel.start_playback() + + if self.mode == Colors.MODE_CANVAS: + # Clear any existing button pressure to avoid a blotch on the screen when entering Canvas mode. + self.cur_buttons = 0 + # Reset the brush. + self.set_brush(self.easel.brush) + + if self.mode == Colors.MODE_SCROLL: + self.scrollref = None + + if self.mode == Colors.MODE_PALETTE: + # This simply darkens the canvas slightly to prepare for an overlay to be drawn on top. + #self.easel.render_overlay() + # Show the brush controls window. + self.easelarea.set_double_buffered(True) + self.brush_controls.set_brush(self.easel.brush) + self.brush_controls.show_all() + self.flush_entire_canvas() + self.overlay_active = True + + if self.mode == Colors.MODE_REFERENCE: + self.easel.render_reference_overlay() + self.flush_entire_canvas() + + def leave_mode (self): + if self.mode == Colors.MODE_INTRO: + self.easel.stop_playback() + self.easel.clear() + self.easel.reset_brush() + self.flush_entire_canvas() + + if self.mode == Colors.MODE_PLAYBACK: + self.easel.stop_playback() + # Progress bar goes to 100 when painting. + self.playbackposbar.ignore_change += 1 + self.playbackpos.set_value(100) + self.playbackposbar.ignore_change -= 1 + + if self.mode == Colors.MODE_CANVAS: + self.end_draw() + + if self.mode == Colors.MODE_SCROLL: + pass + + if self.mode == Colors.MODE_PALETTE: + self.set_brush(self.brush_controls.brush) + #self.easel.clear_overlay() + self.brush_controls.hide() + self.easelarea.set_double_buffered(False) + self.flush_entire_canvas() + self.overlay_active = False + + if self.mode == Colors.MODE_REFERENCE: + self.easel.clear_overlay() + self.flush_entire_canvas() + + def update_mode (self): + if self.mode == None: + self.set_mode(Colors.MODE_INTRO) + + if self.mode == Colors.MODE_INTRO: + self.easel.update_playback() + if not self.easel.playback_done(): + self.flush_dirty_canvas() + if self.cur_buttons & Colors.BUTTON_TOUCH: + self.set_mode(Colors.MODE_CANVAS) + return + + if self.mode == Colors.MODE_PLAYBACK: + self.easel.update_playback() + if not self.easel.playback_done(): + self.flush_dirty_canvas() + # Update the progress bar. + progress_percent = int(100*float(self.easel.playback)/(self.easel.playback_length()+1)) + if self.easel.playing and progress_percent != self.playbackpos.get_value(): + self.playbackposbar.ignore_change += 1 + self.playbackpos.set_value(progress_percent) + self.playbackposbar.ignore_change -= 1 + # Painting during playback mode allows you start where the painter left off. + if self.cur_buttons & Colors.BUTTON_TOUCH: + self.easel.truncate_at_playback() + self.set_mode(Colors.MODE_CANVAS) + return + + if self.mode == Colors.MODE_CANVAS: + # Receive any drawing commands from peers, if not currently drawing. + if not self.easel.stroke: + self.send_and_receive_draw_commands() + # Update drawing. + if self.cur_buttons & Colors.BUTTON_TOUCH: + if self.mx != self.lastmx or self.my != self.lastmy: + mpos = Pos(float(self.mx)/self.width, float(self.my)/self.height) + self.draw(mpos) + self.flush_dirty_canvas() + elif self.easel.stroke: + self.end_draw() + self.flush_dirty_canvas() + # Bring up palette if requested. + if self.cur_buttons & Colors.BUTTON_PALETTE: + self.set_mode(Colors.MODE_PALETTE) + return + # Bring up reference image if requested. + if self.cur_buttons & Colors.BUTTON_REFERENCE: + self.set_mode(Colors.MODE_REFERENCE) + return + # Start scrolling if requested. + if self.cur_buttons & Colors.BUTTON_SCROLL: + self.set_mode(Colors.MODE_SCROLL) + return + + if self.mode == Colors.MODE_SCROLL: + if self.cur_buttons & Colors.BUTTON_TOUCH: + mpos = Pos(self.mx, self.my) + if self.scrollref == None: + self.scrollref = mpos + else: + move = mpos - self.scrollref + if move.x != 0 or move.y != 0: + self.scroll_to(self.scroll - move) + self.scrollref = mpos + else: + self.scrollref = None + # Smoothly pull back towards the image when out of reasonable bounds. + self.snap_scroll() + self.flush_entire_canvas() + if not (self.cur_buttons & Colors.BUTTON_SCROLL): + self.set_mode(Colors.MODE_CANVAS) + + if self.mode == Colors.MODE_PALETTE: + if not self.palette_forced: + if not self.cur_buttons & Colors.BUTTON_PALETTE: + self.set_mode(Colors.MODE_CANVAS) + return + + if self.mode == Colors.MODE_REFERENCE: + if not self.reference_forced: + if not self.cur_buttons & Colors.BUTTON_REFERENCE: + self.set_mode(Colors.MODE_CANVAS) + return + + def set_mode (self, mode): + if self.mode != None: + self.leave_mode() + self.mode = mode + self.enter_mode() + + def update (self): + if self.overlay_active: + return + if self.easel == None: + return + self.update_input() + self.update_mode() + + def tick (self): + # todo- Return False when nothing is going on (to idle the XO), then restart the idle event when something happens. + self.update() + return True + + #----------------------------------------------------------------------------------------------------------------- + # Event handlers + + def on_canvasarea_resize (self): + rect = self.easelarea.get_allocation() + log.debug("Canvas resized to %dx%d", rect[2], rect[3]) + self.width = rect[2] + self.height = rect[3] + + # Rebuild easelimage. + self.easelimage = gtk.gdk.Image(gtk.gdk.IMAGE_FASTEST, gtk.gdk.visual_get_system(), rect[2], rect[3]) + + # Reconfigure canvas. + self.easel.resize(rect[2]/2, rect[3]/2) + + # Resize panels. + self.brush_controls.set_size_request(rect[2], rect[3]) + self.progress.set_size_request(rect[2], rect[3]) + + return True + + def on_canvasarea_expose (self, widget, event): + if not self.easelarea.bin_window: + return True + + rect = self.easelarea.get_allocation() + if self.easelimage is None or (rect[2] != self.width or rect[3] != self.height): + self.on_canvasarea_resize() + + gc = self.easelarea.get_style().fg_gc[gtk.STATE_NORMAL] + + # Blit dirty rectangle of canvas into the image. + self.easel.blit_2x( + self.easelimage, + int(event.area.x/2), int(event.area.y/2), int(event.area.width/2), int(event.area.height/2), + self.overlay_active) + + # Then draw the image to the screen. + self.easelarea.bin_window.draw_image( + gc, self.easelimage, + event.area.x, event.area.y, + event.area.x, event.area.y, event.area.width, event.area.height) + + # Draw introduction text. + if self.mode == Colors.MODE_INTRO: + context = self.easelarea.create_pango_context() + layout = self.easelarea.create_pango_layout(_('Click anywhere to begin painting!')) + layout.set_font_description(pango.FontDescription('Times 14')) + size = layout.get_size() + x = (self.width-size[0]/pango.SCALE)/2 + y = self.height-50-size[1]/pango.SCALE + self.easelarea.bin_window.draw_layout(gc, x, y, layout) + + self.draw_cursor() + + # Hack to keep toolbar up to date. For some reason it fails to draw pretty often. + #self.toolbox.queue_draw() + + def draw_cursor (self): + r = int(self.easel.brush.size*2*self.pressure/256) + self.easelarea.bin_window.draw_arc(self.easelarea.get_style().black_gc, False, self.mx-r/2, self.my-r/2, r, r, 0, 360*64) + self.lastr = r + self.lastmx = self.mx + self.lastmy = self.my + + def on_palette (self, button): + if button.get_active(): + self.set_mode(Colors.MODE_PALETTE) + self.palette_forced = True + else: + self.set_mode(Colors.MODE_CANVAS) + self.palette_forced = False + + def on_take_reference (self, button): + self.take_reference = True + + def on_show_reference (self, button): + if button.get_active(): + self.set_mode(Colors.MODE_REFERENCE) + self.reference_forced = True + else: + self.set_mode(Colors.MODE_CANVAS) + self.reference_forced = False + + def on_videopaint (self, button): + self.videopaint_enabled = button.get_active() + if button.get_active(): + gobject.timeout_add(33, self.on_videopaint_tick) + + def on_videopaintpreview_expose (self, widget, event): + self.easel.blit_videopaint(self.videopaintimage) + size = self.videopaintpreview.window.get_size() + x = (size[0]-self.easel.VIDEO_WIDTH)/2 + y = (size[1]-self.easel.VIDEO_HEIGHT)/2 + self.videopaintpreview.window.draw_image( + self.videopaintpreview.get_style().fg_gc[gtk.STATE_NORMAL], + self.videopaintimage, + 0, 0, x, y, -1, -1) + + def on_videopaint_tick (self): + if not self.videopaint_enabled: + return False + + # Update "pressure" from image (uses blob size). + self.pressure = self.easel.videopaint_pressure + + # Move the mouse to follow the videopaint cursor. + # Note that we do not allow it to reach the corners, to avoid bringing up the Sugar overlay. + size = self.window.get_size() + self.mx = int(25+max(0.0, min(1.0, self.easel.videopaint_pos.x))*(size[0]-50)) + self.my = int(25+max(0.0, min(1.0, self.easel.videopaint_pos.y))*(size[1]-50)) + #gtk.gdk.display_get_default().warp_pointer(self.get_screen(), self.mx, self.my) + self.flush_cursor() + + # Update the preview window. + self.videopaintpreview.queue_draw() + + # Process drawing. + self.update() + + return True + + def on_clear (self, button): + if self.mode != Colors.MODE_CANVAS: + self.set_mode(Colors.MODE_CANVAS) + self.easel.clear() + self.flush_entire_canvas() + + def on_play (self, button): + # Change to playback mode when the Play button is first pressed. + if self.mode != Colors.MODE_PLAYBACK: + self.set_mode(Colors.MODE_PLAYBACK) + # Resume playback. + self.easel.resume_playback() + + def on_pause (self, button): + if self.mode != Colors.MODE_PLAYBACK: + self.set_mode(Colors.MODE_PLAYBACK) + self.easel.pause_playback() + + def on_skip_begin (self, button): + if self.mode != Colors.MODE_PLAYBACK: + self.set_mode(Colors.MODE_PLAYBACK) + self.playbackpos.set_value(0) + + def on_skip_end (self, button): + if self.mode != Colors.MODE_PLAYBACK: + self.set_mode(Colors.MODE_PLAYBACK) + self.playbackpos.set_value(100) + + def on_sample (self, button): + self.set_mode(Colors.MODE_PLAYBACK) + self.easel.clear() + self.easel.load(button.filename) + self.easel.set_playback_speed(8) + self.flush_entire_canvas() + self.toolbox.set_current_toolbar(2) # Switch to 'watch' toolbar. + + def on_playbackposbar_change (self, progress): + if self.playbackposbar.ignore_change > 0: + return + if self.mode != Colors.MODE_PLAYBACK: + self.set_mode(Colors.MODE_PLAYBACK) + self.play_to_playbackpos() + self.easel.pause_playback() + + #----------------------------------------------------------------------------------------------------------------- + # Journal integration + + def read_file(self, file_path): + log.debug("Loading from journal %s", file_path) + self.set_mode(Colors.MODE_CANVAS) + self.easel.clear() + self.easel.load(file_path.encode()) + log.debug("Playing back %d commands", self.easel.playback_length()) + self.set_mode(Colors.MODE_PLAYBACK) + self.playbackpos.set_value(100) + + def write_file(self, file_path): + log.debug("Saving to journal %s", file_path) + log.debug("Saving %d commands", self.easel.playback_length()) + self.easel.save(file_path.encode()) + + def take_screenshot (self): + if self.easelarea and self.easelarea.bin_window: + self._preview.take_screenshot(self.easelarea.bin_window) + + def save_thumbnail(self, filename): + pbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, self.width, self.height) + pbuf = pbuf.get_from_image(self.easelimage, self.easelarea.get_colormap(), 0, 0, 0, 0, self.width, self.height) + pbuf = pbuf.scale_simple(80, 60, gtk.gdk.INTERP_BILINEAR) + pbuf.save(filename, "png") + + #----------------------------------------------------------------------------------------------------------------- + # Benchmarking + # + # Simply causes the C++ code to do a bunch of work and prints out the time used. Useful for testing the benefits + # of optimization. + + def benchmark (self): + # Benchmark a Canvas object. + canvas = Canvas(600, 400) + canvas.clear() + canvas.load(activity.get_bundle_path() + "/data/intro.drw") + start = time.time() + for i in range(0,100): + canvas.start_playback() + canvas.finish_playback() + log.debug("Canvas playback benchmark: %f sec", time.time()-start) + + #canvasimage = gtk.gdk.Image(gtk.gdk.IMAGE_FASTEST, gtk.gdk.visual_get_system(), 600, 400) + #start = time.time() + #for i in range(0,100): + # canvas.blit_2x(canvasimage, 0, 0, 600, 400) + #log.debug("Canvas 1.0x blit benchmark: %f sec", time.time()-start) + + canvasimage = gtk.gdk.Image(gtk.gdk.IMAGE_FASTEST, gtk.gdk.visual_get_system(), 1200, 800) + start = time.time() + for i in range(0,100): + canvas.blit_2x(canvasimage, 0, 0, 600, 400) + log.debug("Canvas 2.0x blit benchmark: %f sec", time.time()-start) + + # Benchmark a Palette object. + palette = Palette(500) + paletteimage = gtk.gdk.Image(gtk.gdk.IMAGE_FASTEST, gtk.gdk.visual_get_system(), 500, 500) + start = time.time() + for i in range(0,100): + palette.render_wheel(paletteimage) + log.debug("Palette wheel benchmark: %f sec", time.time()-start) + + start = time.time() + for i in range(0,100): + palette.render_triangle(paletteimage) + log.debug("Palette triangle benchmark: %f sec", time.time()-start) + -- cgit v0.9.1