Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xSpeak.activity/activity.py11
-rw-r--r--Speak.activity/chat.py54
-rw-r--r--Speak.activity/chatbox.py18
-rw-r--r--Speak.activity/collab.py95
-rw-r--r--Speak.activity/face.py37
-rw-r--r--Speak.activity/messenger.py110
6 files changed, 286 insertions, 39 deletions
diff --git a/Speak.activity/activity.py b/Speak.activity/activity.py
index ddfcfd5..ca933e4 100755
--- a/Speak.activity/activity.py
+++ b/Speak.activity/activity.py
@@ -59,13 +59,15 @@ import voice
import face
from toolbars import ChatToolbar
from chat import Chat
+from collab import CollabActivity
+from messenger import Messenger, SERVICE
CHAT_TOOLBAR = 3
-class SpeakActivity(activity.Activity):
+class SpeakActivity(CollabActivity):
def __init__(self, handle):
- activity.Activity.__init__(self, handle)
+ CollabActivity.__init__(self, SERVICE, handle)
bounds = self.get_allocation()
# pick a voice that espeak supports
@@ -450,11 +452,14 @@ class SpeakActivity(activity.Activity):
def _toolbar_changed_cb(self, widget, index):
if index == CHAT_TOOLBAR:
- self.chat.update(self.face.status)
+ self.chat.me.update(self.face.status)
self.notebook.set_current_page(1)
else:
self.notebook.set_current_page(0)
+ def on_tube(self, tube_conn, initiating):
+ self.chat.messenger = Messenger(tube_conn, initiating, self.chat)
+
#def on_quit(self, data=None):
# self.audio.on_quit()
diff --git a/Speak.activity/chat.py b/Speak.activity/chat.py
index 9dae48b..fa78c91 100644
--- a/Speak.activity/chat.py
+++ b/Speak.activity/chat.py
@@ -24,8 +24,11 @@ import eye
import glasses
import mouth
import face
+import messenger
from chatbox import ChatBox
+logger = logging.getLogger('speak')
+
BUDDY_SIZE = min(100, min(gtk.gdk.screen_width(),
gtk.gdk.screen_height() - style.LARGE_ICON_SIZE) / 5)
BUDDY_XPAD = 0
@@ -42,8 +45,11 @@ ENTRY_YPAD = 7
class Chat(hippo.Canvas):
def __init__(self):
hippo.Canvas.__init__(self)
- self._buddies = {}
+ self.messenger = None
+ self.me = None
+
+ self._buddies = {}
self.connect('motion_notify_event', self._motion_notify_cb)
# buddies box
@@ -62,10 +68,9 @@ class Chat(hippo.Canvas):
# chat entry
- self.chat = ChatBox()
- self.me = self.chat.owner
-
- self.my_face, self_face = self._new_face(self.chat.owner, ENTRY_COLOR)
+ self._chat = ChatBox()
+ self.me, my_face_widget = self._new_face(self._chat.owner,
+ ENTRY_COLOR)
chat_post = gtk.TextView()
chat_post.modify_bg(gtk.STATE_INSENSITIVE,
@@ -95,14 +100,14 @@ class Chat(hippo.Canvas):
)
chat_entry.props.orientation = hippo.ORIENTATION_HORIZONTAL
chat_entry.props.border_color = style.COLOR_WHITE.get_int()
- chat_entry.append(self_face)
+ chat_entry.append(my_face_widget)
chat_entry.append(chat_post_box, hippo.PACK_EXPAND)
chat_box = hippo.CanvasBox(
orientation = hippo.ORIENTATION_VERTICAL,
background_color = style.COLOR_WHITE.get_int(),
)
- chat_box.append(self.chat, hippo.PACK_EXPAND)
+ chat_box.append(self._chat, hippo.PACK_EXPAND)
chat_box.append(chat_entry)
# desk
@@ -113,28 +118,24 @@ class Chat(hippo.Canvas):
self.set_root(self._desk)
- self._add_buddy(self.chat.owner)
-
- def update(self, status, buddy = None, text = None):
- if not buddy or buddy == self.me:
- face = self.my_face
+ def post(self, buddy, status, text):
+ i = self._buddies.get(buddy)
+ if i:
+ face = i['face']
else:
- i = self._buddies.get(buddy)
- if i:
- face = i['face']
- else:
- self._add_buddy(boddy)
- face = self._buddies[buddy]['face']
+ self._add_buddy(buddy)
+ face = self._buddies[buddy]['face']
if status:
face.update(status)
if text:
+ self._chat.add_text(buddy, text)
face.say(text)
def farewell(self, buddy):
i = self._buddies.get(buddy)
if not i:
- logging.debug('farewell: cannot find boddy %s' % buddy.props.nick)
+ logger.debug('farewell: cannot find buddy %s' % buddy.props.nick)
return
self._buddies_list.remove(i['box'])
@@ -173,15 +174,14 @@ class Chat(hippo.Canvas):
if event.keyval == gtk.keysyms.Return:
if not (event.state & gtk.gdk.CONTROL_MASK):
text = widget.get_buffer().props.text
- self.chat.add_text(None, text)
- widget.get_buffer().props.text = ''
- #if len(self._buddies) == 1:
- # self._del_buddy(self.chat.owner)
- #else:
- # self._add_buddy(self.chat.owner)
+ if text:
+ self._chat.add_text(None, text)
+ widget.get_buffer().props.text = ''
+ self.me.say(text)
+ if self.messenger:
+ self.messenger.post(text)
- self.update(None, None, text)
return True
return False
@@ -218,7 +218,7 @@ class Chat(hippo.Canvas):
return (buddy_face, outer)
def _look_at(self, x, y):
- self.my_face.look_at(x, y)
+ self.me.look_at(x, y)
for i in self._buddies.values():
i['face'].look_at(x, y)
diff --git a/Speak.activity/chatbox.py b/Speak.activity/chatbox.py
index d098ca5..f9fb2fd 100644
--- a/Speak.activity/chatbox.py
+++ b/Speak.activity/chatbox.py
@@ -31,6 +31,8 @@ from sugar.graphics.style import (Color, COLOR_BLACK, COLOR_WHITE)
from sugar.graphics.menuitem import MenuItem
from sugar.activity.activity import get_activity_root
+logger = logging.getLogger('speak')
+
URL_REGEXP = re.compile('((http|ftp)s?://)?'
'(([-a-zA-Z0-9]+[.])+[-a-zA-Z0-9]{2,}|([0-9]{1,3}[.]){3}[0-9]{1,3})'
'(:[1-9][0-9]{0,4})?(/[-a-zA-Z0-9/%~@&_+=;:,.?#]*[a-zA-Z0-9/])?')
@@ -238,7 +240,7 @@ class ChatBox(hippo.CanvasScrollbars):
from sugar import profile
from sugar.activity.activity import show_object_in_journal
from sugar.datastore import datastore
- logging.debug('Create journal entry for URL: %s', url)
+ logger.debug('Create journal entry for URL: %s', url)
jobject = datastore.create()
metadata = {
'title': "%s: %s" % (_('URL from Chat'), url),
@@ -300,7 +302,7 @@ class URLMenu(Palette):
pass
def _copy_to_clipboard_cb(self, menuitem):
- logging.debug('Copy %s to clipboard', self.url)
+ logger.debug('Copy %s to clipboard', self.url)
clipboard = gtk.clipboard_get()
targets = [("text/uri-list", 0, 0),
("UTF8_STRING", 0, 1)]
@@ -309,23 +311,23 @@ class URLMenu(Palette):
self._clipboard_data_get_cb,
self._clipboard_clear_cb,
(self.url)):
- logging.error('GtkClipboard.set_with_data failed!')
+ logger.error('GtkClipboard.set_with_data failed!')
else:
self.owns_clipboard = True
def _clipboard_data_get_cb(self, clipboard, selection, info, data):
- logging.debug('_clipboard_data_get_cb data=%s target=%s', data,
+ logger.debug('_clipboard_data_get_cb data=%s target=%s', data,
selection.target)
if selection.target in ['text/uri-list']:
if not selection.set_uris([data]):
- logging.debug('failed to set_uris')
+ logger.debug('failed to set_uris')
else:
- logging.debug('not uri')
+ logger.debug('not uri')
if not selection.set_text(data):
- logging.debug('failed to set_text')
+ logger.debug('failed to set_text')
def _clipboard_clear_cb(self, clipboard, data):
- logging.debug('clipboard_clear_cb')
+ logger.debug('clipboard_clear_cb')
self.owns_clipboard = False
def url_check_protocol(url):
diff --git a/Speak.activity/collab.py b/Speak.activity/collab.py
new file mode 100644
index 0000000..4638d25
--- /dev/null
+++ b/Speak.activity/collab.py
@@ -0,0 +1,95 @@
+# 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 sugar.activity.activity import Activity
+from sugar.presence.sugartubeconn import SugarTubeConnection
+
+logger = logging.getLogger('speak')
+
+class CollabActivity(Activity):
+ def __init__(self, service, *args):
+ Activity.__init__(self, *args)
+ self.service = service
+
+ 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 on_tube(self, tube_conn, initiating):
+ pass
+
+ 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])
+
+ self.on_tube(tube_conn, self._initiating)
diff --git a/Speak.activity/face.py b/Speak.activity/face.py
index 7dbaa77..e77fe8f 100644
--- a/Speak.activity/face.py
+++ b/Speak.activity/face.py
@@ -34,6 +34,7 @@ import logging
import gtk
import gobject
import pango
+import cjson
from gettext import gettext as _
# try:
@@ -59,6 +60,8 @@ import voice
import fft_mouth
import waveform_mouth
+logger = logging.getLogger('speak')
+
PITCH_MAX = 100
RATE_MAX = 100
FACE_PAD = 2
@@ -71,6 +74,38 @@ class Status:
self.eyes = [eye.Eye] * 2
self.mouth = mouth.Mouth
+ def serialize(self):
+ eyes = { eye.Eye : 1,
+ glasses.Glasses : 2 }
+ mouths = { mouth.Mouth : 1,
+ fft_mouth.FFTMouth : 2,
+ waveform_mouth.WaveformMouth : 3 }
+
+ return cjson.encode({
+ 'voice' : { 'language' : self.voice.language,
+ 'gender' : self.voice.gender,
+ 'name' : self.voice.name },
+ 'pitch' : self.pitch,
+ 'rate' : self.rate,
+ 'eyes' : [eyes[i] for i in self.eyes],
+ 'mouth' : mouths[self.mouth] })
+
+ def deserialize(self, buf):
+ eyes = { 1: eye.Eye,
+ 2: glasses.Glasses }
+ mouths = { 1: mouth.Mouth,
+ 2: fft_mouth.FFTMouth,
+ 3: waveform_mouth.WaveformMouth }
+
+ data = cjson.decode(buf)
+ self.voice.language = data['voice']['language']
+ self.voice.gender = data['voice']['gender']
+ self.voice.name = data['voice']['name']
+ self.pitch = data['pitch']
+ self.rate = data['rate']
+ self.eyes = [eyes[i] for i in data['eyes']]
+ self.mouth = mouths[data['mouth']]
+
class View(gtk.EventBox):
def __init__(self, fill_color=style.COLOR_BUTTON_GREY):
gtk.EventBox.__init__(self)
@@ -154,7 +189,7 @@ class View(gtk.EventBox):
if self._audio is None:
return
- logging.debug('%s: %s' % (self.status.voice.name, something))
+ logger.debug('%s: %s' % (self.status.voice.name, something))
pitch = int(self.status.pitch)
rate = int(self.status.rate)
diff --git a/Speak.activity/messenger.py b/Speak.activity/messenger.py
new file mode 100644
index 0000000..cf19701
--- /dev/null
+++ b/Speak.activity/messenger.py
@@ -0,0 +1,110 @@
+# 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
+from dbus.gobject_service import ExportedGObject
+from dbus.service import method, signal
+
+from sugar.presence import presenceservice
+
+import face
+
+logger = logging.getLogger('speak')
+
+SERVICE = 'org.sugarlabs.Speak'
+IFACE = SERVICE
+PATH = '/org/sugarlabs/Speak'
+
+class Messenger(ExportedGObject):
+ def __init__(self, tube, is_initiator, chat):
+ ExportedGObject.__init__(self, tube, PATH)
+
+ self.is_initiator = is_initiator
+ self.chat = chat
+
+ self._tube = tube
+ self._entered = False
+ self._buddies = {}
+
+ self._tube.add_signal_receiver(self._ping_cb, '_ping', IFACE, path=PATH,
+ sender_keyword='sender')
+ self._tube.add_signal_receiver(self._post_cb, '_post', IFACE, path=PATH,
+ sender_keyword='sender')
+ self._tube.watch_participants(self._participant_change_cb)
+
+ def post(self, text):
+ self._post(self.chat.me.status.serialize(), text)
+
+ def _participant_change_cb(self, added, removed):
+ if removed:
+ for i in removed:
+ buddy = self._buddies.get(i)
+ if buddy:
+ logger.debug('buddy %s left chat'
+ % self._buddies[i].props.nick)
+ self.chat.farewell(self._buddies[i])
+ del self._buddies[i]
+ else:
+ logger.warning('_participant_change_cb: cannot find buddy')
+ else:
+ if not self._entered:
+ self.me = self._tube.get_unique_name()
+ if not self.is_initiator:
+ self._ping(self.chat.me.status.serialize())
+ self._entered = True
+
+ @signal(IFACE, signature='s')
+ def _ping(self, status):
+ logger.debug('send ping')
+ pass
+
+ @signal(IFACE, signature='ss')
+ def _post(self, status, text):
+ logger.debug('send message: %s' % text)
+ pass
+
+ @method(dbus_interface=IFACE, in_signature='s', out_signature='',
+ sender_keyword='sender')
+ def _pong(self, sender_status, sender=None):
+ tp_handle = self._tube.bus_name_to_handle[sender]
+ buddy = self._buddies[tp_handle] = self._tube.get_buddy(tp_handle)
+
+ logger.debug('pong received from %s(%s)' % (sender, buddy.props.nick))
+
+ self.chat.post(buddy, face.Status().deserialize(sender_status), None)
+
+ def _ping_cb(self, sender_status, sender=None):
+ if sender == self.me:
+ return
+
+ tp_handle = self._tube.bus_name_to_handle[sender]
+ buddy = self._buddies[tp_handle] = self._tube.get_buddy(tp_handle)
+
+ logger.debug('ping received from %s(%s)' % (sender, buddy.props.nick))
+
+ self.chat.post(buddy, face.Status().deserialize(sender_status), None)
+ remote_object = self._tube.get_object(sender, PATH)
+ remote_object._pong(self.chat.me.status.serialize())
+
+ def _post_cb(self, sender_status, text, sender=None):
+ if sender == self.me:
+ return
+
+ tp_handle = self._tube.bus_name_to_handle[sender]
+ buddy = self._buddies[tp_handle]
+
+ logger.debug('message received from %s(%s): %s'
+ % (sender, buddy.props.nick, text))
+
+ self.chat.post(buddy, face.Status().deserialize(sender_status), text)