From da1df8da094da605786faeb2fa6d7ba5ab6b4928 Mon Sep 17 00:00:00 2001 From: Aleksey Lim Date: Mon, 09 Feb 2009 10:23:32 +0000 Subject: Add collab code --- diff --git a/activity.py b/activity.py index b272763..c00f2b8 100644 --- a/activity.py +++ b/activity.py @@ -12,17 +12,12 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -from sugar.activity import activity -from sugar.presence import presenceservice -from sugar.presence.tubeconn import TubeConnection -import telepathy -import telepathy.client -from dbus import Interface -from dbus.service import method, signal -from dbus.gobject_service import ExportedGObject +import gtk from gettext import gettext as _ -from sugar.activity.activity import get_activity_root +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.toggletoolbutton import ToggleToolButton +from sugar.activity.activity import ActivityToolbox import montage import lessons @@ -30,25 +25,21 @@ import document import char import ground import sound -from toolbars import * +from shared import SharedActivity +from messenger import Messenger, SERVICE +from utils import * -SERVICE = 'org.freedesktop.Telepathy.Tube.Connect' -IFACE = SERVICE -PATH = '/org/freedesktop/Telepathy/Tube/Connect' - -#TMPDIR = os.path.join(get_activity_root(), 'tmp') - -class CartoonBuilderActivity(activity.Activity): +class CartoonBuilderActivity(SharedActivity): def __init__(self, handle): - activity.Activity.__init__(self,handle) - self.notebook = gtk.Notebook() + SharedActivity.__init__(self, self.notebook, SERVICE, handle) + + self.connect('init', self._init_cb) + self.connect('tube', self._tube_cb) + self.notebook.show() self.notebook.props.show_border = False self.notebook.props.show_tabs = False - # XXX do it after(possible) read_file() invoking - # have to rely on calling read_file() from map_cb in sugar-toolkit - self.notebook.connect_after('map', self._map_cb) self.montage = montage.View() self.notebook.append_page(self.montage) @@ -56,7 +47,7 @@ class CartoonBuilderActivity(activity.Activity): self.lessons.show() self.notebook.append_page(self.lessons) - toolbox = activity.ActivityToolbox(self) + toolbox = ActivityToolbox(self) toolbox.show() toolbox.connect('current-toolbar-changed', self._toolbar_changed_cb) self.set_toolbox(toolbox) @@ -70,35 +61,6 @@ class CartoonBuilderActivity(activity.Activity): toolbox.add_toolbar(_('Lessons'), lessons_bar) toolbox.set_current_toolbar(1) - self.set_canvas(self.notebook) - - """ - # mesh stuff - self.pservice = presenceservice.get_instance() - owner = self.pservice.get_owner() - self.owner = owner - try: - name, path = self.pservice.get_preferred_connection() - self.tp_conn_name = name - self.tp_conn_path = path - self.conn = telepathy.client.Connection(name, path) - except TypeError: - pass - self.initiating = None - - #sharing stuff - self.game = None - self.connect('shared', self._shared_cb) - if self._shared_activity: - # we are joining the activity - self.connect('joined', self._joined_cb) - if self.get_shared(): - # oh, OK, we've already joined - self._joined_cb() - else: - # we are creating the activity - pass - """ def read_file(self, filepath): document.load(filepath) @@ -109,177 +71,94 @@ class CartoonBuilderActivity(activity.Activity): def write_file(self, filepath): document.save(filepath) - def _map_cb(self, widget): + def _init_cb(self, widget): self.montage.restore() + def _tube_cb(self, activity, tube_conn, initiating): + self.messenger = Messenger(tube_conn, initiating, self.montage) + def _toolbar_changed_cb(self, widget, index): if index == 2: self.notebook.set_current_page(1) else: self.notebook.set_current_page(0) - - - - - - - def _shared_cb(self,activity): - self.initiating = True - self._setup() - id = self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube( - SERVICE, {}) - #self.app.export.set_label('Shared Me') - - def _joined_cb(self,activity): - if self.game is not None: - return - - if not self._shared_activity: - return - - #for buddy in self._shared_activity.get_joined_buddies(): - # self.buddies_panel.add_watcher(buddy) - - #logger.debug('Joined an existing Connect game') - #self.app.export.set_label('Joined You') - self.initiating = False - self._setup() - - #logger.debug('This is not my activity: waiting for a tube...') - self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes( - reply_handler=self._list_tubes_reply_cb, - error_handler=self._list_tubes_error_cb) - - def _setup(self): - if self._shared_activity is None: +class MontageToolbar(gtk.Toolbar): + def __init__(self): + gtk.Toolbar.__init__(self) + + self.playButton = ToggleToolButton('media-playback-start') + self.playButton.connect('toggled', self._play_cb) + self.insert(self.playButton, -1) + self.playButton.set_tooltip(_('Play / Pause')) + + # Play button Image + self.playButtonImg = gtk.Image() + self.playButtonImg.show() + self.playButtonImg.set_from_icon_name('media-playback-start', gtk.ICON_SIZE_LARGE_TOOLBAR) + + # Pause button Image + self.pauseButtonImg = gtk.Image() + self.pauseButtonImg.show() + self.pauseButtonImg.set_from_icon_name('media-playback-pause', gtk.ICON_SIZE_LARGE_TOOLBAR) + + tempo = TempoSlider(0, 10) + tempo.adjustment.connect("value-changed", self._tempo_cb) + tempo.set_size_request(250, -1) + tempo.set_value(5) + tempo_item = gtk.ToolItem() + tempo_item.add(tempo) + self.insert(tempo_item, -1) + + separator = gtk.SeparatorToolItem() + self.insert(separator,-1) + + clear_tape = ToolButton('sl-reset') + clear_tape.connect('clicked', self._clear_tape_cb) + clear_tape.set_tooltip(_('')) + self.insert(clear_tape, -1) + + self.show_all() + + def _clear_tape_cb(self, widget): + montage.clear_tape() + + def _tempo_cb(self, widget): + montage.set_tempo(widget.value) + + def _play_cb(self, widget): + if widget.get_active(): + widget.set_icon_widget(self.pauseButtonImg) + sound.play() + montage.play() + else: + widget.set_icon_widget(self.playButtonImg) + sound.stop() + montage.stop() + +class LessonsToolbar(gtk.Toolbar): + def __init__(self): + gtk.Toolbar.__init__(self) + self._mask = False + + for lesson in lessons.THEMES: + button = gtk.ToggleToolButton() + button.set_label(lesson.name) + button.connect('clicked', self._lessons_cb, lesson) + self.insert(button, -1) + + self.get_nth_item(0).set_active(True) + self.show_all() + + def _lessons_cb(self, widget, lesson): + if self._mask: return + self._mask = True - bus_name, conn_path, channel_paths = self._shared_activity.get_channels() - - # Work out what our room is called and whether we have Tubes already - room = None - tubes_chan = None - text_chan = None - for channel_path in channel_paths: - channel = telepathy.client.Channel(bus_name, channel_path) - htype, handle = channel.GetHandle() - if htype == telepathy.HANDLE_TYPE_ROOM: - #logger.debug('Found our room: it has handle#%d "%s"', - # handle, self.conn.InspectHandles(htype, [handle])[0]) - room = handle - ctype = channel.GetChannelType() - if ctype == telepathy.CHANNEL_TYPE_TUBES: - #logger.debug('Found our Tubes channel at %s', channel_path) - tubes_chan = channel - elif ctype == telepathy.CHANNEL_TYPE_TEXT: - #logger.debug('Found our Text channel at %s', channel_path) - text_chan = channel - - if room is None: - #logger.error("Presence service didn't create a room") - return - if text_chan is None: - #logger.error("Presence service didn't create a text channel") - return + for i, j in enumerate(lessons.THEMES): + if j != lesson: + self.get_nth_item(i).set_active(False) - # Make sure we have a Tubes channel - PS doesn't yet provide one - if tubes_chan is None: - #logger.debug("Didn't find our Tubes channel, requesting one...") - tubes_chan = self.conn.request_channel(telepathy.CHANNEL_TYPE_TUBES, - telepathy.HANDLE_TYPE_ROOM, room, True) - - self.tubes_chan = tubes_chan - self.text_chan = text_chan - - tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube', - self._new_tube_cb) - - def _list_tubes_reply_cb(self, tubes): - for tube_info in tubes: - self._new_tube_cb(*tube_info) - - def _list_tubes_error_cb(self, e): - #logger.error('ListTubes() failed: %s', e) - pass - - def _new_tube_cb(self, id, initiator, type, service, params, state): - #logger.debug('New tube: ID=%d initator=%d type=%d service=%s ' - # 'params=%r state=%d', id, initiator, type, service, - # params, state) - - if (self.game is None and type == telepathy.TUBE_TYPE_DBUS and - service == SERVICE): - if state == telepathy.TUBE_STATE_LOCAL_PENDING: - self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].AcceptDBusTube(id) - - tube_conn = TubeConnection(self.conn, - self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES], - id, group_iface=self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP]) - self.game = ConnectGame(tube_conn, self.initiating, self) - -class ConnectGame(ExportedGObject): - def __init__(self,tube, is_initiator, activity): - super(ConnectGame,self).__init__(tube,PATH) - self.tube = tube - self.is_initiator = is_initiator - self.entered = False - self.activity = activity - - self.ordered_bus_names=[] - self.tube.watch_participants(self.participant_change_cb) - - def participant_change_cb(self, added, removed): - if not self.entered: - if self.is_initiator: - self.add_hello_handler() - else: - self.Hello() - self.entered = True - - @signal(dbus_interface=IFACE,signature='') - def Hello(self): - """Request that this player's Welcome method is called to bring it - up to date with the game state. - """ - - @method(dbus_interface=IFACE, in_signature='s', out_signature='') - def Welcome(self, sdata): - #sdata is the zip file contents - #self.activity.app.lessonplans.set_label('got data to restore') - self.activity.app.restore(str(sdata)) - - def add_hello_handler(self): - self.tube.add_signal_receiver(self.hello_cb, 'Hello', IFACE, - path=PATH, sender_keyword='sender') - - def hello_cb(self, sender=None): - self.tube.get_object(sender, PATH).Welcome(self.activity.app.getsdata(),dbus_interface=IFACE) - -""" - def getsdata(self): - #self.lessonplans.set_label('getting sdata') - # THE BELOW SHOULD WORK BUT DOESN'T - #zf = StringIO.StringIO() - #self.savetozip(zf) - #zf.seek(0) - #sdata = zf.read() - #zf.close() - # END OF STUFF THAT DOESN'T WORK - sdd = {} - tmpbgpath = os.path.join(TMPDIR,'back.png') - self.bgpixbuf.save(tmpbgpath,'png') - sdd['pngdata'] = file(tmpbgpath).read() - os.remove(tmpbgpath) - sdd['fgpixbufpaths'] = self.fgpixbufpaths - #sdd['fgpixbufs'] = [] - #count = 1 - #for pixbuf in self.fgpixbufs: - # filename = '%02d.png' % count - # filepath = os.path.join(TMPDIR,filename) - # pixbuf.save(filepath,'png') - # sdd['fgpixbufs'].append(file(filepath).read()) - # os.remove(filepath) - # count += 1 - return pickle.dumps(sdd) -""" + widget.props.active = True + lesson.change() + self._mask = False diff --git a/char.py b/char.py index 7855161..c91c9b9 100644 --- a/char.py +++ b/char.py @@ -31,10 +31,11 @@ def load(): class Frame: def __init__(self, id): self.id = id + self.name = '' self._thumb = None self._orig = None - def read(self): + def serialize(self): if self._orig: return pixbuf2str(self._orig) else: @@ -100,9 +101,11 @@ class CustomFrame(Frame): def select(self): if self._orig: return True; - self.id, self._orig = theme.choose(lambda jobject: (jobject.object_id, - theme.pixbuf(jobject.file_path)), (None, None)) - if self.id: + self.name, self.id, self._orig = theme.choose(lambda jobject: + (jobject.metadata['title'], jobject.object_id, + theme.pixbuf(jobject.file_path)), + (None, None, None)) + if self.name: self._thumb = theme.scale(self._orig) return True else: @@ -117,14 +120,16 @@ class Char: for i in sorted(glob.glob(theme.path(dir, '*'))): self.frames.append(PreinstalledFrame( os.path.join(dir, os.path.basename(i)))) - for i in range(len(self.frames)-1, - theme.FRAME_ROWS*theme.FRAME_COLS): - self.frames.append(EmptyFrame()) self._thumb = theme.pixbuf(thumbfile, theme.THUMB_SIZE) + self._custom = False else: for i in range(0, theme.FRAME_ROWS*theme.FRAME_COLS): self.frames.append(CustomFrame()) self._thumb = theme.CUSTOM_FRAME_THUMB + self._custom = True + + def custom(self): + return self._custom def thumb(self): return self._thumb diff --git a/document.py b/document.py index 8d43895..9273f2c 100644 --- a/document.py +++ b/document.py @@ -47,7 +47,7 @@ def save(filepath): if value.custom(): node['custom'] = True node['filename'] = arcname - zip.writestr(arcname, value.read()) + zip.writestr(arcname, value.serialize()) else: node['custom'] = False node['name'] = unicode(value.name) @@ -60,7 +60,7 @@ def save(filepath): [i for i in set(Document.tape) if not i.empty() and i.custom()]): arcname = 'frame%03d.png' % i cfg['frames'][frame.id] = arcname - zip.writestr(arcname, frame.read()) + zip.writestr(arcname, frame.serialize()) for i, frame in enumerate(Document.tape): if not frame.empty(): diff --git a/ground.py b/ground.py index 4aef242..1f97514 100644 --- a/ground.py +++ b/ground.py @@ -22,7 +22,7 @@ def load(): from document import Document if Document.ground and Document.ground.custom(): - THEMES.insert(-1, Document.ground) + THEMES.append(Document.ground) class Ground: def __init__(self, name, id): @@ -33,7 +33,7 @@ class Ground: def custom(self): return True - def read(self): + def serialize(self): return theme.pixbuf2str(self._orig) def thumb(self): @@ -75,6 +75,7 @@ class JournalGround(Ground): def __init__(self, jobject): Ground.__init__(self, jobject.metadata['title'], jobject.object_id) self._orig = theme.pixbuf(jobject.file_path) + THEMES.append(self) THEMES = [ PreinstalledGround(_('Saturn'), 'images/backpics/bigbg01.gif'), diff --git a/messenger.py b/messenger.py new file mode 100644 index 0000000..f034ee8 --- /dev/null +++ b/messenger.py @@ -0,0 +1,284 @@ +# 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 + +import cjson +import logging +import dbus +from dbus.gobject_service import ExportedGObject +from dbus.service import method, signal + +from sugar.presence import presenceservice + +import char +import ground +import sound +from document import Document + +logger = logging.getLogger('cartoon-builder') + +SERVICE = 'org.sugarlabs.CartoonBuilder' +IFACE = SERVICE +PATH = '/org/sugarlabs/CartoonBuilder' + +class Slot: + def __init__(self, sender=None, raw=None): + if sender: + data = cjson.decode(raw) + self.seqno = data['seqno'] + self.oid = data['oid'] + self.sender = sender + else: + self.seqno = -1 + self.oid = None + self.sender = None + + def serialize(self): + return cjson.encode({ + 'seqno': self.seqno, + 'oid' : self.oid}) + +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._view.connect('ground-changed', self._ground_changed_cb) + self._view.connect('sound-changed', self._sound_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() + + slots = [('%s:%d' % (FRAME, i), f) \ + for i, f in enumerate(Document.tape)] + \ + [(GROUND, Document.ground), (SOUND, Document.sound)] + for i in slots: + self._slots[i[0]] = Slot() + + if self.initiator: + self._tube.add_signal_receiver(self._ping_cb, '_ping', IFACE, + path=PATH, sender_keyword='sender') + for i in slots: + slot = self._slots[i[0]] + slot.seqno = 0 + slot.oid = i[1].id + slot.sender = 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='ss') + def _notify(self, slot, raw): + pass + + # the whole list of slots for incomers + @method(dbus_interface=IFACE, in_signature='', out_signature='a{ss}', + sender_keyword='sender') + def _snapshot(self, sender=None): + logger.debug('_snapshot requested from %s' % sender) + out = {} + + for i, slot in self._slots.items(): + out[i] = slot.serialize() + + return out + + # fetch content of specified object + @method(dbus_interface=IFACE, in_signature='ss', out_signature='say', + sender_keyword='sender', byte_arrays=True) + def _fetch(self, type, oid, sender=None): + logger.debug('_fetch requested from %s type=%s oid=%s' \ + % (sender, type, oid)) + return object_serialize(type, oid) + + 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 slot, raw in rawlist.items(): + self._receive(slot, 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, raw, sender=None): + if sender == self.me: + return + logger.debug('_notify requested from %s' % sender) + self._receive(slot, raw, sender) + + def _receive(self, slot, raw, sender): + cur = self._slots[slot] + new = Slot(sender, raw) + + logger.debug('object received slot=%s seqno=%d sender=%s oid=%s from %s' + % (slot, new.seqno, new.sender, new.oid, 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.sender > sender: + logger.debug('current value is higher ranked then arrived') + return + if cur.sender == self.me: + # we sent current and arrived value rewrites it + logger.debug('resend current with higher seqno') + self._send(slot, cur.oid) + return + else: + logger.debug('just discard low rank') + return + else: + logger.debug('accept higher seqno') + + if new.oid and not object_find(slot, new.oid): + remote = self._tube.get_object(sender, PATH) + name, raw = remote._fetch(slot, new.oid, byte_arrays=True) + object_new(slot, new.oid, name, raw) + + object_select(self._view, slot, new.oid) + self._slots[slot] = new + + def _send(self, slot_num, oid): + slot = self._slots[slot_num] + slot.seqno += 1 + slot.sender = self.me + slot.oid = oid + self._notify(slot_num, slot.serialize()) + + logger.debug('_send slot=%s oid=%s seqno=%d' + % (slot_num, oid, slot.seqno)) + + def _frame_changed_cb(self, sender, index, frame): + self._send('%s:%d' % (FRAME, index), frame and frame.id) + + def _ground_changed_cb(self, sender, ground): + self._send(GROUND, ground.id) + + def _sound_changed_cb(self, sender, sound): + self._send(SOUND, sound.id) + +FRAME = 'frame' +GROUND = 'ground' +SOUND = 'sound' + +OBJECTS = { + FRAME : char.THEMES[-1].frames, + GROUND : ground.THEMES, + SOUND : sound.THEMES } + +def object_find(type, oid): + if type.startswith(FRAME): + for c in char.THEMES: + if not c: + continue + for i in c.frames: + if i.id == oid: + return i + else: + for i in OBJECTS[type.split(':')[0]]: + if i and i.id == oid: + return i + return None + +def object_new(type, oid, name, raw): + logger.debug('add new object type=%s oid=%s' % (type, oid)) + + if type.startswith(FRAME): + object = char.RestoredFrame(oid, raw) + for i, frame in enumerate(OBJECTS[FRAME]): + if not frame.id: + OBJECTS[FRAME][i] = object + return + elif type.startswith(GROUND): + object = ground.RestoredGround(name, oid, raw) + elif type.startswith(SOUND): + object = sound.RestoredSound(name, oid, raw) + else: + logger.error('cannot create object of type %s' % type) + return + + OBJECTS[type.split(':')[0]].append(object) + +def object_serialize(type, oid): + object = object_find(type, oid) + + if object: + return (object.name, object.serialize()) + else: + logger.error('cannot find object to serialize type=%s oid=%s' \ + % (type, oid)) + return ('', '') + +def object_select(view, type, oid): + if oid: + object = object_find(type, oid) + else: + object = None + + if type.startswith(FRAME): + index = int(type.split(':')[1]) + view.props.frame = (index, object) + elif type.startswith(GROUND): + view.props.ground = object + elif type.startswith(SOUND): + view.props.sound = object + else: + logger.error('cannot find object to select type=%s oid=%s' % (type, oid)) diff --git a/montage.py b/montage.py index 840d3b7..a92f0d9 100644 --- a/montage.py +++ b/montage.py @@ -20,20 +20,27 @@ import gtk import gobject +import logging +from gobject import SIGNAL_RUN_FIRST, TYPE_PYOBJECT import theme import char import ground import sound from document import Document, clean +from screen import Screen from utils import * +logger = logging.getLogger('cartoon-builder') + def play(): View.play_tape_num = 0 View.playing = gobject.timeout_add(View.delay, _play_tape) def stop(): View.playing = None + View.screen.fgpixbuf = Document.tape[View.tape_selected].orig() + View.screen.draw() def set_tempo(tempo): View.delay = 10 + (10-int(tempo)) * 100 @@ -67,40 +74,10 @@ def _play_tape(): return True class View(gtk.EventBox): - class Screen(gtk.DrawingArea): - def __init__(self): - gtk.DrawingArea.__init__(self) - self.gc = None # initialized in realize-event handler - self.width = 0 # updated in size-allocate handler - self.height = 0 # idem - self.bgpixbuf = None - self.fgpixbuf = None - self.connect('size-allocate', self.on_size_allocate) - self.connect('expose-event', self.on_expose_event) - self.connect('realize', self.on_realize) - - def on_realize(self, widget): - self.gc = widget.window.new_gc() - - def on_size_allocate(self, widget, allocation): - self.height = self.width = min(allocation.width, allocation.height) - - def on_expose_event(self, widget, event): - # This is where the drawing takes place - if self.bgpixbuf: - pixbuf = self.bgpixbuf - if pixbuf.get_width != self.width: - pixbuf = theme.scale(pixbuf, self.width) - widget.window.draw_pixbuf(self.gc, pixbuf, 0, 0, 0, 0, -1, -1, 0, 0) - - if self.fgpixbuf: - pixbuf = self.fgpixbuf - if pixbuf.get_width != self.width: - pixbuf = theme.scale(pixbuf, self.width) - widget.window.draw_pixbuf(self.gc, pixbuf, 0, 0, 0, 0, -1, -1, 0, 0) - - def draw(self): - self.queue_draw() + __gsignals__ = { + 'frame-changed' : (SIGNAL_RUN_FIRST, None, 2*[TYPE_PYOBJECT]), + 'ground-changed': (SIGNAL_RUN_FIRST, None, [TYPE_PYOBJECT]), + 'sound-changed' : (SIGNAL_RUN_FIRST, None, [TYPE_PYOBJECT]) } screen = Screen() play_tape_num = 0 @@ -109,33 +86,116 @@ class View(gtk.EventBox): tape_selected = -1 tape = [] + def set_frame(self, value): + tape_num, frame = value + + if frame == None: + clean(tape_num) + View.tape[tape_num].child.set_from_pixbuf(theme.EMPTY_THUMB) + else: + if not frame.select(): + return False + + Document.tape[tape_num] = frame + View.tape[tape_num].child.set_from_pixbuf(frame.thumb()) + + if frame.custom(): + index = [i for i, f in enumerate(char.THEMES[-1].frames) + if f == frame][0] + if index >= len(self._frames): + first = index / theme.FRAME_COLS * theme.FRAME_COLS + for i in range(first, first + theme.FRAME_COLS): + self._add_frame(i) + + if self.char.custom(): + self._frames[index].set_from_pixbuf(frame.thumb()) + + if View.tape_selected == tape_num: + self._tape_cb(None, None, tape_num) + + return True + + def set_ground(self, value): + self._set_combo(self._ground_combo, value) + + def set_sound(self, value): + self._set_combo(self._sound_combo, value) + + def _set_combo(self, combo, value): + try: + self._stop_emission = True + pos = -1 + + for i, item in enumerate(combo.get_model()): + if item[0] == value: + pos = i + break + + if pos == -1: + combo.append_item(value, text = value.name, + size = (theme.THUMB_SIZE, theme.THUMB_SIZE), + pixbuf = value.thumb()) + pos = len(combo.get_model())-1 + + combo.set_active(pos) + finally: + self._stop_emission = False + + frame = gobject.property(type=object, getter=None, setter=set_frame) + ground = gobject.property(type=object, getter=None, setter=set_ground) + sound = gobject.property(type=object, getter=None, setter=set_sound) + + def restore(self): + def new_combo(themes, cb, object = None, closure = None): + combo = ComboBox() + sel = 0 + + for i, o in enumerate(themes): + if o: + combo.append_item(o, text = o.name, + size = (theme.THUMB_SIZE, theme.THUMB_SIZE), + pixbuf = o.thumb()) + if object and o.name == object.name: + sel = i + else: + combo.append_separator() + + combo.connect('changed', cb, closure) + combo.set_active(sel) + combo.show() + + return combo + + self.controlbox.pack_start(new_combo(char.THEMES, self._char_cb), + False, False) + self._ground_combo = new_combo(ground.THEMES, self._combo_cb, + Document.ground, self._ground_cb) + self.controlbox.pack_start(self._ground_combo, False, False) + self._sound_combo = new_combo(sound.THEMES, self._combo_cb, + Document.sound, self._sound_cb) + self.controlbox.pack_start(self._sound_combo, False, False) + + for i in range(theme.TAPE_COUNT): + View.tape[i].child.set_from_pixbuf(Document.tape[i].thumb()) + self._tape_cb(None, None, 0) + + return False + def __init__(self): gtk.EventBox.__init__(self) + self.char = None self._frames = [] self._prev_combo_selected = {} + self._stop_emission = False # frames table - self.table = gtk.Table(theme.FRAME_ROWS, columns=theme.FRAME_COLS, + self.table = gtk.Table(#theme.FRAME_ROWS, columns=theme.FRAME_COLS, homogeneous=False) - for y in range(theme.FRAME_ROWS): - for x in range(theme.FRAME_COLS): - image = gtk.Image() - self._frames.append(image) - - image_box = gtk.EventBox() - image_box.set_events(gtk.gdk.BUTTON_PRESS_MASK) - image_box.connect('button_press_event', self._frame_cb, - y * theme.FRAME_COLS + x) - image_box.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(BLACK)) - image_box.modify_bg(gtk.STATE_PRELIGHT, gtk.gdk.color_parse(BLACK)) - image_box.props.border_width = 2 - image_box.set_size_request(theme.THUMB_SIZE, theme.THUMB_SIZE) - image_box.add(image) - - self.table.attach(image_box, x, x+1, y, y+1) + for i in range(theme.FRAME_ROWS * theme.FRAME_COLS): + self._add_frame(i) # frames box @@ -165,7 +225,7 @@ class View(gtk.EventBox): screen_pink.modify_bg(gtk.STATE_NORMAL,gtk.gdk.color_parse(PINK)) screen_box = gtk.EventBox() screen_box.set_border_width(5) - screen_box.add(self.screen) + screen_box.add(View.screen) screen_pink.add(screen_box) screen_pink.props.border_width = theme.BORDER_WIDTH @@ -263,44 +323,37 @@ class View(gtk.EventBox): self.add(greenbox) self.show_all() - def restore(self): - def new_combo(themes, cb, object = None, closure = None): - combo = ComboBox() - sel = 0 - - for i, o in enumerate(themes): - if o: - combo.append_item(o, text = o.name, - size = (theme.THUMB_SIZE, theme.THUMB_SIZE), - pixbuf = o.thumb()) - if object and o.name == object.name: - sel = i - else: - combo.append_separator() + def _add_frame(self, index): + y = index / theme.FRAME_COLS + x = index - y*theme.FRAME_COLS + logger.debug('add new frame x=%d y=%d index=%d' % (x, y, index)) - combo.connect('changed', cb, closure) - combo.set_active(sel) - combo.show() + image = gtk.Image() + image.show() + image.set_from_pixbuf(theme.EMPTY_THUMB) + self._frames.append(image) - return combo + image_box = gtk.EventBox() + image_box.set_events(gtk.gdk.BUTTON_PRESS_MASK) + image_box.connect('button_press_event', self._frame_cb, index) + image_box.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(BLACK)) + image_box.modify_bg(gtk.STATE_PRELIGHT, gtk.gdk.color_parse(BLACK)) + image_box.props.border_width = 2 + image_box.set_size_request(theme.THUMB_SIZE, theme.THUMB_SIZE) + image_box.add(image) - self.controlbox.pack_start(new_combo(char.THEMES, self._char_cb), - False, False) - self.controlbox.pack_start(new_combo(ground.THEMES, self._combo_cb, - Document.ground, self._ground_cb), False, False) - self.controlbox.pack_start(new_combo(sound.THEMES, self._combo_cb, - Document.sound, self._sound_cb), False, False) + if self.char and self.char.custom(): + image_box.show() - for i in range(theme.TAPE_COUNT): - View.tape[i].child.set_from_pixbuf(Document.tape[i].thumb()) - self._tape_cb(None, None, 0) + self.table.attach(image_box, x, x+1, y, y+1) - return False + return image def _tape_cb(self, widget, event, index): if event and event.button == 3: - clean(index) - View.tape[index].child.set_from_pixbuf(theme.EMPTY_THUMB) + self.set_frame((index, None)) + self.emit('frame-changed', index, None) + return tape = View.tape[index] tape.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(YELLOW)) @@ -315,26 +368,32 @@ class View(gtk.EventBox): gtk.gdk.color_parse(BLACK)) View.tape_selected = index - self.screen.fgpixbuf = Document.tape[index].orig() - self.screen.draw() + View.screen.fgpixbuf = Document.tape[index].orig() + View.screen.draw() def _frame_cb(self, widget, event, i): - if event.button == 3: self.char.clean(i) self._frames[i].set_from_pixbuf(self.char.frames[i].thumb()) else: - frame = self.char.frames[i] - if frame.select(): - Document.tape[View.tape_selected] = frame - View.tape[View.tape_selected].child.set_from_pixbuf(frame.thumb()) - self._frames[i].set_from_pixbuf(frame.thumb()) - self._tape_cb(None, None, View.tape_selected) + if i < len(self.char.frames): + frame = self.char.frames[i] + if not self.set_frame((View.tape_selected, frame)): + return + else: + frame = None + self.set_frame((View.tape_selected, None)) + + self.emit('frame-changed', View.tape_selected, frame) def _char_cb(self, widget, closure): self.char = widget.props.value for i in range(len(self._frames)): - self._frames[i].set_from_pixbuf(self.char.frames[i].thumb()) + if i < len(self.char.frames): + self._frames[i].set_from_pixbuf(self.char.frames[i].thumb()) + self._frames[i].parent.show() + else: + self._frames[i].parent.hide() def _combo_cb(self, widget, cb): choice = widget.props.value.select() @@ -344,22 +403,25 @@ class View(gtk.EventBox): return if id(choice) != id(widget.props.value): - pos = widget.get_active() widget.append_item(choice, text = choice.name, size = (theme.THUMB_SIZE, theme.THUMB_SIZE), - pixbuf = choice.thumb(), position = pos) - widget.set_active(pos) + pixbuf = choice.thumb()) + widget.set_active(len(widget.get_model())-1) self._prev_combo_selected[widget] = widget.get_active() cb(choice) def _ground_cb(self, choice): - self.screen.bgpixbuf = choice.orig() - self.screen.draw() + View.screen.bgpixbuf = choice.orig() + View.screen.draw() Document.ground = choice + if not self._stop_emission: + self.emit('ground-changed', choice) def _sound_cb(self, choice): Document.sound = choice + if not self._stop_emission: + self.emit('sound-changed', choice) def _screen_size_cb(self, widget, aloc): size = min(aloc.width, aloc.height) diff --git a/screen.py b/screen.py new file mode 100644 index 0000000..29ed2b7 --- /dev/null +++ b/screen.py @@ -0,0 +1,58 @@ +# 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 +# + +### cartoonbuilder +### +### author: Ed Stoner (ed@whsd.net) +### (c) 2007 World Wide Workshop Foundation + +import gtk + +import theme + +class Screen(gtk.DrawingArea): + def __init__(self): + gtk.DrawingArea.__init__(self) + self.gc = None # initialized in realize-event handler + self.width = 0 # updated in size-allocate handler + self.height = 0 # idem + self.bgpixbuf = None + self.fgpixbuf = None + self.connect('size-allocate', self.on_size_allocate) + self.connect('expose-event', self.on_expose_event) + self.connect('realize', self.on_realize) + + def on_realize(self, widget): + self.gc = widget.window.new_gc() + + def on_size_allocate(self, widget, allocation): + self.height = self.width = min(allocation.width, allocation.height) + + def on_expose_event(self, widget, event): + # This is where the drawing takes place + if self.bgpixbuf: + pixbuf = self.bgpixbuf + if pixbuf.get_width != self.width: + pixbuf = theme.scale(pixbuf, self.width) + widget.window.draw_pixbuf(self.gc, pixbuf, 0, 0, 0, 0, -1, -1, 0, 0) + + if self.fgpixbuf: + pixbuf = self.fgpixbuf + if pixbuf.get_width != self.width: + pixbuf = theme.scale(pixbuf, self.width) + widget.window.draw_pixbuf(self.gc, pixbuf, 0, 0, 0, 0, -1, -1, 0, 0) + + def draw(self): + self.queue_draw() diff --git a/shared.py b/shared.py new file mode 100644 index 0000000..98fb965 --- /dev/null +++ b/shared.py @@ -0,0 +1,130 @@ +# 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 + +import logging +import telepathy +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') + +class CanvasActivity(Activity): + __gsignals__ = { + 'init' : (SIGNAL_RUN_FIRST, None, []) } + + def __init__(self, canvas, *args): + Activity.__init__(self, *args) + + self._inited = False + + # XXX do it after(possible) read_file() invoking + # have to rely on calling read_file() from map_cb in sugar-toolkit + canvas.connect_after('map', self._map_cb) + self.set_canvas(canvas) + + def get_inited(self): + return self._inited + + inited = property(type=bool, default=False, getter=get_inited, setter=None) + + def _map_cb(self, widget): + self._inited = True + self.emit('init') + +class SharedActivity(CanvasActivity): + __gsignals__ = { + 'tube' : (SIGNAL_RUN_FIRST, None, 2*[TYPE_PYOBJECT]) } + + def __init__(self, canvas, service, *args): + CanvasActivity.__init__(self, canvas, *args) + + self.service = service + self._postpone_tubes = [] + + self.connect('init', self._init_sharedactivity_cb) + self.connect('shared', self._shared_cb) + + # Owner.props.key + if self._shared_activity: + # We are joining the activity + self.connect('joined', self._joined_cb) + if self.get_shared(): + # We've already joined + self._joined_cb() + + def _init_sharedactivity_cb(self): + for i in self._postpone_tubes: + self.emit('tube', i, self._initiating) + self._postpone_tubes = [] + + def _shared_cb(self, activity): + logger.debug('My activity was shared') + self._initiating = True + self._sharing_setup() + + logger.debug('This is my activity: making a tube...') + id = self._tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube( + self.service, {}) + + def _joined_cb(self, activity): + if not self._shared_activity: + return + + logger.debug('Joined an existing shared activity') + + self._initiating = False + self._sharing_setup() + + logger.debug('This is not my activity: waiting for a tube...') + self._tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes( + reply_handler=self._list_tubes_reply_cb, + error_handler=self._list_tubes_error_cb) + + def _sharing_setup(self): + if self._shared_activity is None: + logger.error('Failed to share or join activity') + return + 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 + + self._tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube', self._new_tube_cb) + + def _list_tubes_reply_cb(self, tubes): + for tube_info in tubes: + self._new_tube_cb(*tube_info) + + def _list_tubes_error_cb(self, e): + logger.error('ListTubes() failed: %s', e) + + def _new_tube_cb(self, id, initiator, type, service, params, state): + logger.debug('New tube: ID=%d initator=%d type=%d service=%s ' + 'params=%r state=%d', id, initiator, type, service, + params, state) + + if (type == telepathy.TUBE_TYPE_DBUS and + service == self.service): + if state == telepathy.TUBE_STATE_LOCAL_PENDING: + self._tubes_chan[telepathy.CHANNEL_TYPE_TUBES].AcceptDBusTube(id) + + tube_conn = SugarTubeConnection(self._conn, + self._tubes_chan[telepathy.CHANNEL_TYPE_TUBES], + id, group_iface=self._text_chan[telepathy.CHANNEL_INTERFACE_GROUP]) + + if self.get_inited(): + self.emit('tube', tube_conn, self._initiating) + else: + self._postpone_tubes.append(tube_conn) diff --git a/sound.py b/sound.py index 7beb9e8..7819b93 100644 --- a/sound.py +++ b/sound.py @@ -27,7 +27,7 @@ def load(): from document import Document if Document.sound and Document.sound.custom(): - THEMES.insert(-1, Document.sound) + THEMES.append(Document.sound) class Sound: playing = False @@ -43,7 +43,7 @@ class Sound: def custom(self): return True - def read(self): + def serialize(self): return file(self._soundfile, 'r').read() def thumb(self): @@ -67,12 +67,12 @@ class PreinstalledSound(Sound): class MuteSound(Sound): def __init__(self, name): - Sound.__init__(self, name, None, None, theme.SOUND_MUTE) + Sound.__init__(self, name, 'mute', None, theme.SOUND_MUTE) def custom(self): return False - def read(self): + def serialize(self): return '' def select(self): @@ -101,6 +101,7 @@ class JournalSound(Sound): Sound.__init__(self, jobject.metadata['title'], jobject.object_id, soundfile, theme.SOUND_CUSTOM) shutil.copy(jobject.file_path, soundfile) + THEMES.append(self) THEMES = [ PreinstalledSound(_('Gobble'), 'sounds/gobble.wav'), diff --git a/toolbars.py b/toolbars.py deleted file mode 100644 index 98e1bbb..0000000 --- a/toolbars.py +++ /dev/null @@ -1,104 +0,0 @@ -# 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 - -import gtk -from gettext import gettext as _ - -from sugar.graphics.toolbutton import ToolButton -from sugar.graphics.toggletoolbutton import ToggleToolButton - -import montage -import lessons -import sound -from utils import * - -class MontageToolbar(gtk.Toolbar): - def __init__(self): - gtk.Toolbar.__init__(self) - - self.playButton = ToggleToolButton('media-playback-start') - self.playButton.connect('toggled', self._play_cb) - self.insert(self.playButton, -1) - self.playButton.set_tooltip(_('Play / Pause')) - - # Play button Image - self.playButtonImg = gtk.Image() - self.playButtonImg.show() - self.playButtonImg.set_from_icon_name('media-playback-start', gtk.ICON_SIZE_LARGE_TOOLBAR) - - # Pause button Image - self.pauseButtonImg = gtk.Image() - self.pauseButtonImg.show() - self.pauseButtonImg.set_from_icon_name('media-playback-pause', gtk.ICON_SIZE_LARGE_TOOLBAR) - - tempo = TempoSlider(0, 10) - tempo.adjustment.connect("value-changed", self._tempo_cb) - tempo.set_size_request(250, -1) - tempo.set_value(5) - tempo_item = gtk.ToolItem() - tempo_item.add(tempo) - self.insert(tempo_item, -1) - - separator = gtk.SeparatorToolItem() - self.insert(separator,-1) - - clear_tape = ToolButton('sl-reset') - clear_tape.connect('clicked', self._clear_tape_cb) - clear_tape.set_tooltip(_('')) - self.insert(clear_tape, -1) - - self.show_all() - - def _clear_tape_cb(self, widget): - montage.clear_tape() - - def _tempo_cb(self, widget): - montage.set_tempo(widget.value) - - def _play_cb(self, widget): - if widget.get_active(): - widget.set_icon_widget(self.pauseButtonImg) - sound.play() - montage.play() - else: - widget.set_icon_widget(self.playButtonImg) - sound.stop() - montage.stop() - -class LessonsToolbar(gtk.Toolbar): - def __init__(self): - gtk.Toolbar.__init__(self) - self._mask = False - - for lesson in lessons.THEMES: - button = gtk.ToggleToolButton() - button.set_label(lesson.name) - button.connect('clicked', self._lessons_cb, lesson) - self.insert(button, -1) - - self.get_nth_item(0).set_active(True) - self.show_all() - - def _lessons_cb(self, widget, lesson): - if self._mask: - return - self._mask = True - - for i, j in enumerate(lessons.THEMES): - if j != lesson: - self.get_nth_item(i).set_active(False) - - widget.props.active = True - lesson.change() - self._mask = False -- cgit v0.9.1