From 8de9dc0e92b47375e44c4edfad926b8e5143c60f Mon Sep 17 00:00:00 2001 From: Walter Bender Date: Sun, 19 Jan 2014 17:42:51 +0000 Subject: adding support for collaboration -- so far, just circle objects --- diff --git a/NEWS b/NEWS index b6233c1..026936d 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,8 @@ +15 + +ENHANCEMENTS: +* Added support for collaboration + 14 ENHANCEMENTS: diff --git a/activity.py b/activity.py index d63574e..ef86d6b 100644 --- a/activity.py +++ b/activity.py @@ -31,6 +31,7 @@ import pygame import sugargame import sugargame.canvas +from gi.repository import GObject from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GdkPixbuf @@ -46,10 +47,24 @@ from sugar3.graphics.toolbarbox import ToolbarButton from sugar3.graphics.style import GRID_CELL_SIZE from sugar3.datastore import datastore from sugar3.graphics.objectchooser import get_preview_pixbuf +from sugar3.graphics.alert import Alert +from sugar3.graphics.icon import Icon +from sugar3.graphics.xocolor import XoColor +from sugar3 import profile + +import telepathy +from dbus.service import signal +from dbus.gobject_service import ExportedGObject +from sugar3.presence import presenceservice +from sugar3.presence.tubeconn import TubeConnection import tools import physics +SERVICE = 'org.laptop.physics' +IFACE = SERVICE +PATH = '/org/laptop/physics' + class PhysicsActivity(activity.Activity): def __init__(self, handle): @@ -63,7 +78,10 @@ class PhysicsActivity(activity.Activity): self._canvas = sugargame.canvas.PygameCanvas(self) self.game = physics.main(self) + self.preview = None + + self._constructors = {} self.build_toolbar() self.set_canvas(self._canvas) @@ -73,6 +91,25 @@ class PhysicsActivity(activity.Activity): logging.debug(os.path.join( activity.get_activity_root(), 'data', 'data')) self._canvas.run_pygame(self.game.run) + GObject.idle_add(self._setup_sharing) + + def _setup_sharing(self): + self.we_are_sharing = False + + if self.shared_activity: + # We're joining + if not self.get_shared(): + xocolors = XoColor(profile.get_color().to_string()) + share_icon = Icon(icon_name='zoom-neighborhood', + xo_color=xocolors) + self._joined_alert = Alert() + self._joined_alert.props.icon = share_icon + self._joined_alert.props.title = _('Please wait') + self._joined_alert.props.msg = _('Starting connection...') + self.add_alert(self._joined_alert) + self._waiting_cursor() + + self._setup_presence_service() def __configure_cb(self, event): ''' Screen size has changed ''' @@ -104,7 +141,8 @@ class PhysicsActivity(activity.Activity): return preview_data def build_toolbar(self): - self.max_participants = 1 + self.max_participants = 4 + toolbar_box = ToolbarBox() activity_button = ActivityToolbarButton(self) toolbar_box.toolbar.insert(activity_button, 0) @@ -216,6 +254,9 @@ class PhysicsActivity(activity.Activity): _insert_item(create_toolbar, button, -1) button.show() self.radioList[button] = c.name + if hasattr(c, 'constructor'): + self._constructors[c.name] = \ + self.game.toolList[c.name].constructor def __icon_path(self, name): activity_path = activity.get_bundle_path() @@ -292,7 +333,6 @@ class PhysicsActivity(activity.Activity): pygame.event.post(pygame.event.Event(pygame.USEREVENT, action='clear_all')) if len(self.game.world.world.GetBodyList()) > 2: - print len(self.game.world.world.GetBodyList()) clear_all_alert = ConfirmationAlert() clear_all_alert.props.title = _('Are You Sure?') clear_all_alert.props.msg = \ @@ -359,3 +399,153 @@ class PhysicsActivity(activity.Activity): if event.changed_mask & Gdk.WindowState.ICONIFIED: pygame.event.post(pygame.event.Event(pygame.USEREVENT, action='focus_out')) + + def _setup_presence_service(self): + ''' Setup the Presence Service. ''' + self.pservice = presenceservice.get_instance() + + owner = self.pservice.get_owner() + self.owner = owner + self.buddies = [owner] + self._share = '' + self.connect('shared', self._shared_cb) + self.connect('joined', self._joined_cb) + + def _shared_cb(self, activity): + ''' Either set up initial share...''' + if self.get_shared_activity() is None: + logging.error('Failed to share or join activity ... \ + shared_activity is null in _shared_cb()') + return + + self.initiating = True + self.waiting = False + self.we_are_sharing = True + logging.debug('I am sharing...') + + 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) + + logging.debug('This is my activity: making a tube...') + id = self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube( + SERVICE, {}) + + def _joined_cb(self, activity): + ''' ...or join an exisiting share. ''' + if self.get_shared_activity() is None: + logging.error('Failed to share or join activity ... \ + shared_activity is null in _shared_cb()') + return + + self.initiating = False + logging.debug('I joined a shared activity.') + + 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) + + logging.debug('I am joining an 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) + + self.waiting = True + + if self._joined_alert is not None: + self.remove_alert(self._joined_alert) + self._joined_alert = None + self._restore_cursor() + self.we_are_sharing = True + + def _restore_cursor(self): + ''' No longer waiting, so restore standard cursor. ''' + if not hasattr(self, 'get_window'): + return + self.get_window().set_cursor(self.old_cursor) + + def _waiting_cursor(self): + ''' Waiting, so set watch cursor. ''' + if not hasattr(self, 'get_window'): + return + self.old_cursor = self.get_window().get_cursor() + self.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.WATCH)) + + def _list_tubes_reply_cb(self, tubes): + ''' Reply to a list request. ''' + for tube_info in tubes: + self._new_tube_cb(*tube_info) + + def _list_tubes_error_cb(self, e): + ''' Log errors. ''' + logging.error('ListTubes() failed: %s', e) + + def _new_tube_cb(self, id, initiator, type, service, params, state): + ''' Create a new tube. ''' + logging.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 == 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.chattube = ChatTube(tube_conn, self.initiating, \ + self.event_received_cb) + + def event_received_cb(self, text): + ''' Data is passed as tuples: cmd:text ''' + dispatch_table = {'C': self._construct_shared_circle, + } + logging.debug('<<< %s' % (text[0])) + dispatch_table[text[0]](text[2:]) + + def _construct_shared_circle(self, data): + circle_data = json.loads(data) + pos = circle_data[0] + radius = circle_data[1] + density = circle_data[2] + restitution = circle_data[3] + friction = circle_data[4] + self._constructors['Circle'](pos, radius, density, restitution, + friction, share=False) + + def send_event(self, text): + ''' Send event through the tube. ''' + if hasattr(self, 'chattube') and self.chattube is not None: + logging.debug('>>> %s' % (text[0])) + self.chattube.SendText(text) + + +class ChatTube(ExportedGObject): + ''' Class for setting up tube for sharing ''' + def __init__(self, tube, is_initiator, stack_received_cb): + super(ChatTube, self).__init__(tube, PATH) + self.tube = tube + self.is_initiator = is_initiator # Are we sharing or joining activity? + self.stack_received_cb = stack_received_cb + self.stack = '' + + self.tube.add_signal_receiver(self.send_stack_cb, 'SendText', IFACE, + path=PATH, sender_keyword='sender') + + def send_stack_cb(self, text, sender=None): + if sender == self.tube.get_unique_name(): + return + self.stack = text + self.stack_received_cb(text) + + @signal(dbus_interface=IFACE, signature='s') + def SendText(self, text): + self.stack = text diff --git a/activity/activity.info b/activity/activity.info index 8e118f3..b9f346b 100755 --- a/activity/activity.info +++ b/activity/activity.info @@ -4,6 +4,6 @@ summary = Prove Sir Isaac Newton right! Create real life simulations using diffe bundle_id = org.laptop.physics exec = sugar-activity activity.PhysicsActivity icon = activity-physics -activity_version = 14 +activity_version = 15 show_launcher = yes mime_types = application/x-physics-activity; diff --git a/tools.py b/tools.py index 1d0211f..7a59475 100644 --- a/tools.py +++ b/tools.py @@ -185,13 +185,21 @@ class CircleTool(Tool): self.pt1 = tuple_to_int(event.pos) elif event.type == MOUSEBUTTONUP: if event.button == 1: - self.game.world.add.ball( - self.pt1, self.radius, - dynamic=True, density=self.palette_data['density'], - restitution=self.palette_data['restitution'], - friction=self.palette_data['friction']) + self.constructor(self.pt1, self.radius, + self.palette_data['density'], + self.palette_data['restitution'], + self.palette_data['friction']) self.pt1 = None + def constructor(self, pos, radius, density, restitution, friction, + share=True): + self.game.world.add.ball(pos, radius, dynamic=True, + density=density, restitution=restitution, + friction=friction) + if share and self.game.activity.we_are_sharing: + data = json.dumps([pos, radius, density, restitution, friction]) + self.game.activity.send_event('C:' + data) + def draw(self): Tool.draw(self) # Draw a circle from pt1 to mouse -- cgit v0.9.1