From c71d67bc8af32cf791a8e6c90102dfd831181644 Mon Sep 17 00:00:00 2001 From: Aleksey Lim Date: Wed, 11 Feb 2009 21:43:37 +0000 Subject: Implement collab --- diff --git a/activity.py b/activity.py index 78af8d8..e209e1a 100644 --- a/activity.py +++ b/activity.py @@ -22,7 +22,7 @@ from sugar.graphics.toolbutton import ToolButton import model import montage import lessons -import messenger +from messenger import Messenger, SERVICE from shared import SharedActivity from theme import * from utils import * @@ -30,7 +30,9 @@ from utils import * class flipsticksActivity(SharedActivity): def __init__(self, handle): self.notebook = gtk.Notebook() - SharedActivity.__init__(self, self.notebook, messenger.SERVICE, handle) + SharedActivity.__init__(self, self.notebook, SERVICE, handle) + + self.connect('tube', self._tube_cb) self.notebook.show() self.notebook.props.show_border = False @@ -70,6 +72,9 @@ class flipsticksActivity(SharedActivity): else: self.notebook.set_current_page(0) + def _tube_cb(self, activity, tube_conn, initiating): + self.messenger = Messenger(tube_conn, initiating, self.montage) + class MontageToolbar(gtk.Toolbar): def __init__(self, montage): gtk.Toolbar.__init__(self) diff --git a/messenger.py b/messenger.py index 1bad33f..10701c5 100644 --- a/messenger.py +++ b/messenger.py @@ -12,6 +12,165 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +import dbus +import pickle +import cjson +import logging +from dbus.gobject_service import ExportedGObject +from dbus.service import method, signal + +from sugar.presence import presenceservice + +import model + +logger = logging.getLogger('flipsticks') + SERVICE = 'org.freedesktop.Telepathy.Tube.Connect' IFACE = SERVICE PATH = '/org/freedesktop/Telepathy/Tube/Connect' + +class Slot: + def __init__(self, seqno=-1, owner=None): + self.seqno = seqno + self.owner = owner + +class Messenger(ExportedGObject): + def __init__(self, tube, initiator, view): + ExportedGObject.__init__(self, tube, PATH) + + self.initiator = initiator + self._tube = tube + self._entered = False + self._slots = [] + self._view = view + + self._view.connect('frame-changed', self._frame_changed_cb) + self._tube.watch_participants(self._participant_change_cb) + + def _participant_change_cb(self, added, removed): + if not self._entered and added: + self.me = self._tube.get_unique_name() + + for i in range(len(model.keys)): + self._slots.append(Slot()) + + if self.initiator: + self._tube.add_signal_receiver(self._ping_cb, '_ping', IFACE, + path=PATH, sender_keyword='sender') + for i in self._slots: + i.seqno = 0 + i.owner = self.me + else: + self._pong_handle = self._tube.add_signal_receiver( + self._pong_cb, '_pong', IFACE, path=PATH, + sender_keyword='sender') + self._ping() + + self._tube.add_signal_receiver(self._notify_cb, '_notify', IFACE, + path=PATH, sender_keyword='sender') + self._entered = True + + # incomers' signal to retrieve initial snapshot + @signal(IFACE, signature='') + def _ping(self): + logger.debug('send ping') + pass + + # object is ready to post snapshot to incomer + @signal(IFACE, signature='') + def _pong(self): + logger.debug('send pong') + pass + + # slot was changed + @signal(IFACE, signature='iiss') + def _notify(self, slot, seqno, sender, raw): + pass + + # the whole list of slots for incomers + @method(dbus_interface=IFACE, in_signature='', out_signature='a(iss)', + sender_keyword='sender') + def _snapshot(self, sender=None): + logger.debug('_snapshot requested from %s' % sender) + out = [] + + for i, slot in enumerate(self._slots): + out.append((slot.seqno, slot.owner, + cjson.encode(model.keys[i].collect()))) + + return out + + def _ping_cb(self, sender=None): + if sender == self.me: + return + logger.debug('_ping received from %s' % sender) + self._pong() + + def _pong_cb(self, sender=None): + if sender == self.me: + return + logger.debug('_pong sent from %s' % sender) + + # we've got source for _snapshot and don't need _pong anymore + self._tube.remove_signal_receiver(self._pong_handle) + self._pong_handle = None + + remote = self._tube.get_object(sender, PATH) + rawlist = remote._snapshot() + + logger.debug('snapshot received len=%d' % len(rawlist)) + + for i, (seqno, owner, raw) in enumerate(rawlist): + self._receive(i, seqno, owner, raw, sender) + + # we are ready to receive _snapshot requests + self._tube.add_signal_receiver(self._ping_cb, '_ping', IFACE, + path=PATH, sender_keyword='sender') + + def _notify_cb(self, slot, seqno, owner, raw, sender=None): + if sender == self.me: + return + logger.debug('_notify requested from %s' % sender) + self._receive(slot, seqno, owner, raw, sender) + + def _receive(self, slot, seqno, owner, raw, sender): + cur = self._slots[slot] + new = Slot(seqno, owner) + + logger.debug('object received slot=%s seqno=%d owner=%s from %s' + % (slot, new.seqno, new.owner, sender)) + + if cur.seqno > new.seqno: + logger.debug('trying to rewrite newer value by older one') + return + elif cur.seqno == new.seqno: + # arrived value was sent at the same time as current one + if cur.owner > new.owner: + logger.debug('current value is higher ranked then arrived') + return + if cur.owner == self.me: + # we sent current and arrived value rewrites it + logger.debug('resend current with higher seqno') + self._send(slot) + return + else: + logger.debug('just discard low rank') + return + else: + logger.debug('accept higher seqno') + + self._view.props.keyframe = (slot, model.StoredFrame(cjson.decode(raw))) + self._slots[slot] = new + + def _send(self, slot_num): + slot = self._slots[slot_num] + slot.seqno += 1 + slot.owner = self.me + + self._notify(slot_num, slot.seqno, slot.owner, + cjson.encode(model.keys[slot_num].collect())) + + logger.debug('_send slot=%s seqno=%d' % (slot_num, slot.seqno)) + + def _frame_changed_cb(self, sender, slot): + self._send(slot) diff --git a/model.py b/model.py index 479af62..6d34dfb 100644 --- a/model.py +++ b/model.py @@ -13,28 +13,81 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import os -import pickle +import cjson +import math -import theme -import kinematic +from theme import * keys = [] class KeyFrame: - def __init__(self): - self.clear() - def empty(self): return self.joints == None - def assign(self, x): - self.middle = x.middle - self.parts = x.parts.copy() - self.sticks = x.sticks.copy() - self.joints = x.joints.copy() - self.scaled_sticks = x.get_scaled_sticks() - self.scaled_joints = x.get_scaled_joints(self.x, - int(theme.KEYFRAMEHEIGHT/2.0)) + def _setjoints(self, joints, sticks, middle): + if self.empty(): + return + + # have to traverse in order because + # parent joints must be set right + for stickname in STICKLIST: + (angle,len) = sticks[stickname] + jname = JOINTS[stickname] + (x,y) = getparentjoint(jname, joints, middle) + parents = getparentsticks(stickname) + panglesum = 0 + for parentname in parents: + (pangle,plen) = sticks[parentname] + panglesum += pangle + (nx,ny) = self._getpoints(x,y,angle+panglesum,len) + joints[jname] = (nx,ny) + + def _getpoints(self, x, y, angle, len): + nx = int(round(x + (len * math.cos(math.radians(angle))))) + ny = int(round(y - (len * math.sin(math.radians(angle))))) + return (nx,ny) + + def _initjoints(self): + joints = {} + for stickname in JOINTS: + jname = JOINTS[stickname] + joints[jname] = (0,0) + return joints + +class StoredFrame(KeyFrame): + def __init__(self, data=None): + if not data: + self.clear() + else: + def array2tuple(a): + return a and (a[0], a[1]) + + def hash2tuple(h): + if not h: + return None + out = {} + for i, j in h.items(): + out[i] = array2tuple(j) + return out + + self.x = scale_keyframe(data['x']) + self.middle = scale_middle(data['middle']) + self.parts = data['parts'] + self.sticks = hash2tuple(data['sticks']) + self.joints = hash2tuple(data['joints']) + self._make_thumbs() + self.setjoints() + + def collect(self): + return { 'x' : unscale_keyframe(self.x), + 'middle' : unscale_middle(self.middle), + 'parts' : self.parts, + 'sticks' : self.sticks, + 'joints' : self.joints } + + def setjoints(self): + if not self.empty(): + self._setjoints(self.joints, self.sticks, self.middle) def clear(self): self.middle = None @@ -51,34 +104,43 @@ class KeyFrame: self.scaled_joints[jname] = (jx+dx, jy) self.x += dx + def assign(self, x): + self.middle = x.middle + self.parts = x.parts.copy() + self.sticks = x.sticks.copy() + self.joints = x.joints.copy() + self._make_thumbs() + + def _make_thumbs(self): + if self.empty(): + self.scaled_sticks = None + self.scaled_joints = None + return + + self.scaled_sticks = self.sticks.copy() + self.scaled_joints = self._initjoints() + + for key in self.scaled_sticks: + (angle,len) = self.scaled_sticks[key] + newlen = int(len * .2) + self.scaled_sticks[key] = (angle,newlen) + + self._setjoints(self.scaled_joints, self.scaled_sticks, + (self.x, KEYFRAMEHEIGHT/2)) + def save(filename): out = [] for i in keys: - out.append({ - 'x' : i.x, - 'middle' : i.middle, - 'parts' : i.parts, - 'sticks' : i.sticks, - 'joints' : i.joints, - 'scaled_sticks' : i.scaled_sticks, - 'scaled_joints' : i.scaled_joints }) + out.append(i.collect()) - file(filename, 'w').write(pickle.dumps(out)) + file(filename, 'w').write(cjson.encode(out)) def load(filename): - inc = pickle.loads(file(filename, 'r').read()) + inc = cjson.decode(file(filename, 'r').read()) for i, data in enumerate(inc): - key = keys[i] - - key.x = data['x'] - key.middle = data['middle'] - key.parts = data['parts'] - key.sticks = data['sticks'] - key.joints = data['joints'] - key.scaled_sticks = data['scaled_sticks'] - key.scaled_joints = data['scaled_joints'] + keys[i] = StoredFrame(data) def getparentsticks(stickname): if stickname in ['RIGHT SHOULDER','LEFT SHOULDER','NECK','TORSO']: @@ -158,7 +220,7 @@ def _get_base64_pixbuf_data(pixbuf): return base64.b64encode(str(data[0])) for i in range(5): - key = KeyFrame() - keyframe_width = theme.KEYFRAMEWIDTH/5 + key = StoredFrame() + keyframe_width = KEYFRAMEWIDTH/5 key.x = keyframe_width/2 + i*keyframe_width keys.append(key) diff --git a/montage.py b/montage.py index b2d8030..db02a97 100644 --- a/montage.py +++ b/montage.py @@ -22,6 +22,7 @@ import gtk import math import gobject import logging +from gobject import SIGNAL_RUN_FIRST, TYPE_PYOBJECT from gettext import gettext as _ from sugar.activity.activity import get_bundle_path @@ -35,6 +36,20 @@ from theme import * logger = logging.getLogger('flipsticks') class View(gtk.EventBox): + __gsignals__ = { + 'frame-changed' : (SIGNAL_RUN_FIRST, None, [TYPE_PYOBJECT]) } + + def set_keyframe(self, value): + i, key = value + logger.debug('set_keyframe[%d]=%s' % (i, key and key.collect())) + if not key: + model.keys[i].clear() + else: + model.keys[i] = key + self.restore() + + keyframe = gobject.property(type=object, getter=None, setter=set_keyframe) + def reset(self): self.key.reset() self.selectstickebox() @@ -43,10 +58,12 @@ class View(gtk.EventBox): def setframe(self): model.keys[self.kfselected].assign(self.key) self.drawkeyframe() + self.emit('frame-changed', self.kfselected) def clearframe(self): model.keys[self.kfselected].clear() self.drawkeyframe() + self.emit('frame-changed', self.kfselected) def setplayspeed(self, value): self.waittime = int((100-value)*5) @@ -113,17 +130,6 @@ class View(gtk.EventBox): model.screen_shot(pixbuf) def restore(self): - # keep keyframes on screen after decreasing screen resolution - for i in model.keys: - if i.x >= KEYFRAMEWIDTH-KEYFRAME_RADIUS: - i.move(KEYFRAMEWIDTH-KEYFRAME_RADIUS - i.x) - - if i.middle[0] >= DRAWWIDTH or i.middle[1] >= DRAWHEIGHT: - tmp = screen.ScreenFrame() - i.middle = (DRAWWIDTH/2, DRAWHEIGHT/3) - tmp.assign(i) - i.assign(tmp) - self.drawkeyframe() self.syncmaintokf() self.updateentrybox() @@ -245,6 +251,17 @@ class View(gtk.EventBox): if frame.x + xdiff > KEYFRAME_RADIUS \ and frame.x + xdiff < KEYFRAMEWIDTH-KEYFRAME_RADIUS: frame.move(xdiff) + + if self._emit_move_handle: + gobject.source_remove(self._emit_move_handle) + if self._emit_move_key != self.kfpressed: + self._emit_move(self._emit_move_key) + + self._emit_move_key = self.kfpressed + self._emit_move_handle = gobject.timeout_add( + MOVEMIT_TIMEOUT, self._emit_move, + self.kfpressed) + self.drawkeyframe() self.kf_mouse_pos = x return True @@ -571,6 +588,7 @@ class View(gtk.EventBox): self.toplevel = activity self.stickselected = 'RIGHT SHOULDER' self.pixmap = None + self._emit_move_handle = None self.setplayspeed(50) @@ -602,6 +620,7 @@ class View(gtk.EventBox): | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK) + self.mfdraw.set_size_request(DRAWWIDTH, DRAWHEIGHT) screen_box = gtk.EventBox() screen_box.modify_bg(gtk.STATE_NORMAL,gtk.gdk.color_parse(BACKGROUND)) @@ -732,7 +751,7 @@ class View(gtk.EventBox): hdesktop = gtk.HBox() hdesktop.pack_start(leftbox, False) - hdesktop.pack_start(screen_pink) + hdesktop.pack_start(screen_pink, False, False) desktop = gtk.VBox() desktop.pack_start(hdesktop) @@ -793,6 +812,11 @@ class View(gtk.EventBox): self.sizeentry.set_text(str(int(adj.value))) self.enterlen_callback(None, self.sizeentry) + def _emit_move(self, key): + self._emit_move_handle = None + self.emit('frame-changed', key) + return False + def _inarea(widget, x, y): x_, y_, width, height = widget.get_allocation() if x < 0 or y < 0 or x >= width or y >= height: diff --git a/screen.py b/screen.py index 31ba47f..bd31199 100644 --- a/screen.py +++ b/screen.py @@ -12,22 +12,20 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -import math - import theme import model -class ScreenFrame: +class ScreenFrame(model.KeyFrame): def __init__(self): self.reset() def setjoints(self): - _setjoints(self.joints, self.sticks, self.middle) + self._setjoints(self.joints, self.sticks, self.middle) def reset(self): self.parts = theme.PARTS.copy() self.sticks = theme.STICKS.copy() - self.joints = _initjoints() + self.joints = self._initjoints() self.middle = (theme.DRAWWIDTH/2, theme.DRAWHEIGHT/3) self.setjoints() @@ -38,23 +36,10 @@ class ScreenFrame: self.joints = x.joints.copy() self.setjoints() - def get_scaled_sticks(self): - out = self.sticks.copy() - for key in out: - (angle,len) = out[key] - newlen = int(len * .2) - out[key] = (angle,newlen) - return out - - def get_scaled_joints(self, x, y): - out = _initjoints() - _setjoints(out, self.get_scaled_sticks(), (x, y)) - return out - def getrotatepoint(self): (angle,len) = self.sticks['TORSO'] x,y = self.middle - (rx,ry) = _getpoints(x,y,angle,int(len/2.0)) + (rx,ry) = self._getpoints(x,y,angle,int(len/2.0)) return (rx,ry) def inrotate(self, x, y): @@ -81,30 +66,3 @@ class ScreenFrame: for jname in self.joints: (jx, jy) = self.joints[jname] self.joints[jname] = (jx+dx, jy+dy) - -def _initjoints(): - joints = {} - for stickname in theme.JOINTS: - jname = theme.JOINTS[stickname] - joints[jname] = (0,0) - return joints - -def _setjoints(joints, sticks, middle): - # have to traverse in order because - # parent joints must be set right - for stickname in theme.STICKLIST: - (angle,len) = sticks[stickname] - jname = theme.JOINTS[stickname] - (x,y) = model.getparentjoint(jname, joints, middle) - parents = model.getparentsticks(stickname) - panglesum = 0 - for parentname in parents: - (pangle,plen) = sticks[parentname] - panglesum += pangle - (nx,ny) = _getpoints(x,y,angle+panglesum,len) - joints[jname] = (nx,ny) - -def _getpoints(x,y,angle,len): - nx = int(round(x + (len * math.cos(math.radians(angle))))) - ny = int(round(y - (len * math.sin(math.radians(angle))))) - return (nx,ny) diff --git a/shared.py b/shared.py index 3ca51ca..6725cf0 100644 --- a/shared.py +++ b/shared.py @@ -19,7 +19,7 @@ from gobject import property, SIGNAL_RUN_FIRST, TYPE_PYOBJECT from sugar.activity.activity import Activity from sugar.presence.sugartubeconn import SugarTubeConnection -logger = logging.getLogger('cartoon-builder') +logger = logging.getLogger('flipsticks') class CanvasActivity(Activity): __gsignals__ = { diff --git a/theme.py b/theme.py index 5bf4b07..3d3b404 100644 --- a/theme.py +++ b/theme.py @@ -14,6 +14,8 @@ import gtk +from sugar.graphics import style + GRAY = "#B7B7B7" # gray PINK = "#FF0099" # pink YELLOW = "#FFFF00" # yellow @@ -96,12 +98,13 @@ LANG = {'English':{'size':'Size', PAD = 10 LOGO_WIDTH = 276 +TOLLBAR_HEIGHT = style.LARGE_ICON_SIZE KEYFRAMEWIDTH = gtk.gdk.screen_width() - PAD*3 KEYFRAMEHEIGHT = 80 DRAWWIDTH = gtk.gdk.screen_width() - LOGO_WIDTH - PAD*4 -DRAWHEIGHT = gtk.gdk.screen_height() - KEYFRAMEHEIGHT - PAD*5 +DRAWHEIGHT = gtk.gdk.screen_height() - KEYFRAMEHEIGHT - PAD*6 - TOLLBAR_HEIGHT KEYFRAMES = [] KEYFRAMES_NUMBER = 5 @@ -109,10 +112,45 @@ TOTALFRAMES = 30 KEYFRAME_RADIUS = 40 +MOVEMIT_TIMEOUT = 1000 + for i in range(KEYFRAMES_NUMBER): keyframe_width = KEYFRAMEWIDTH/KEYFRAMES_NUMBER KEYFRAMES.append(keyframe_width/2 + i*keyframe_width) +# scale coordinates between native resolution and transportable + +TRANSFER_DRAWWIDTH = 350 +TRANSFER_DRAWHEIGHT = 350 +TRANSFER_KEYFRAMEWIDTH = 600 + +def scale_keyframe(x): + factor = float(TRANSFER_KEYFRAMEWIDTH) / (KEYFRAMEWIDTH) + x = max(KEYFRAME_RADIUS, int(x/factor)) + x = min(KEYFRAMEWIDTH-KEYFRAME_RADIUS-1, x) + return x + +def scale_middle(middle): + if not middle: + return None + x_factor = float(TRANSFER_DRAWWIDTH) / DRAWWIDTH + y_factor = float(TRANSFER_DRAWHEIGHT) / DRAWHEIGHT + return (min(DRAWWIDTH-1, int(middle[0]/x_factor)), + min(DRAWHEIGHT-1, int(middle[1]/y_factor))) + +def unscale_keyframe(x): + factor = float(TRANSFER_KEYFRAMEWIDTH) / (KEYFRAMEWIDTH) + return int(x*factor) + +def unscale_middle(middle): + if not middle: + return None + x_factor = float(TRANSFER_DRAWWIDTH) / DRAWWIDTH + y_factor = float(TRANSFER_DRAWHEIGHT) / DRAWHEIGHT + return (int(middle[0]*x_factor), int(middle[1]*y_factor)) + +# defaults + STICKS = {'HEAD':(0,15), 'NECK':(90,15), 'RIGHT SHOULDER':(185,25), @@ -134,7 +172,6 @@ STICKS = {'HEAD':(0,15), PARTS = {'RIGHT HAND':14, 'LEFT HAND':14} - STICKLIST = ['NECK','HEAD','RIGHT SHOULDER','UPPER RIGHT ARM','LOWER RIGHT ARM', 'LEFT SHOULDER','UPPER LEFT ARM','LOWER LEFT ARM','TORSO', 'RIGHT HIP','UPPER RIGHT LEG','LOWER RIGHT LEG','RIGHT FOOT', -- cgit v0.9.1