Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xautogen.sh2
-rw-r--r--configure.ac6
-rw-r--r--po/de.po52
-rw-r--r--po/es.po176
-rw-r--r--po/hi.po205
-rw-r--r--po/it.po30
-rw-r--r--po/ja.po30
-rw-r--r--po/mn.po30
-rw-r--r--po/nl.po44
-rw-r--r--po/tvl.po206
-rw-r--r--po/vi.po30
-rw-r--r--po/zh_TW.po167
-rw-r--r--src/sugar/__init__.py14
-rw-r--r--src/sugar/activity/Makefile.am1
-rw-r--r--src/sugar/activity/__init__py0
-rw-r--r--src/sugar/activity/activity.py180
-rw-r--r--src/sugar/activity/activityfactory.py88
-rw-r--r--src/sugar/activity/activityhandle.py12
-rw-r--r--src/sugar/activity/activityservice.py4
-rw-r--r--src/sugar/activity/bundlebuilder.py2
-rw-r--r--src/sugar/activity/i18n.py144
-rw-r--r--src/sugar/activity/main.py12
-rw-r--r--src/sugar/activity/widgets.py14
-rw-r--r--src/sugar/bundle/activitybundle.py26
-rw-r--r--src/sugar/bundle/bundle.py5
-rw-r--r--src/sugar/bundle/contentbundle.py12
-rw-r--r--src/sugar/datastore/Makefile.am1
-rw-r--r--src/sugar/datastore/datastore.py342
-rw-r--r--src/sugar/datastore/dbus_helpers.py115
-rw-r--r--src/sugar/graphics/alert.py45
-rw-r--r--src/sugar/graphics/icon.py28
-rw-r--r--src/sugar/graphics/palette.py3
-rw-r--r--src/sugar/graphics/palettewindow.py4
-rw-r--r--src/sugar/graphics/style.py1
-rw-r--r--src/sugar/presence/Makefile.am1
-rw-r--r--src/sugar/presence/activity.py707
-rw-r--r--src/sugar/presence/buddy.py373
-rw-r--r--src/sugar/presence/connectionmanager.py117
-rw-r--r--src/sugar/presence/presenceservice.py640
-rw-r--r--src/sugar/profile.py33
-rw-r--r--src/sugar/util.py2
41 files changed, 2379 insertions, 1525 deletions
diff --git a/autogen.sh b/autogen.sh
index 3d12f8f..f25b0a3 100755
--- a/autogen.sh
+++ b/autogen.sh
@@ -3,4 +3,4 @@ export ACLOCAL="aclocal -I m4"
intltoolize
autoreconf -i
-./configure "$@"
+./configure --enable-maintainer-mode "$@"
diff --git a/configure.ac b/configure.ac
index 02f842b..ca4d32c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,4 +1,4 @@
-AC_INIT([sugar-toolkit],[0.87.2],[],[sugar-toolkit])
+AC_INIT([sugar-toolkit],[0.89.5],[],[sugar-toolkit])
AC_PREREQ([2.59])
@@ -7,6 +7,7 @@ AC_CONFIG_SRCDIR([configure.ac])
AM_INIT_AUTOMAKE([1.9 foreign dist-bzip2 no-dist-gzip])
+AM_MAINTAINER_MODE
AC_DISABLE_STATIC
AC_PROG_LIBTOOL
@@ -26,7 +27,8 @@ AC_SUBST(PYGTK_DEFSDIR)
# Setup GETTEXT
#
-ALL_LINGUAS="af am ar ay bg bn bn_IN ca de dz el en es fa fa_AF ff fr gu ha hi ht ig is it ja km ko mk ml mn mr mvo nb ne nl pa pap pis pl ps pt pt_BR qu ro ru rw sd si sl te th tpi tr ur vi yo zh_CN zh_TW"
+ALL_LINGUAS="af am ar ay bg bi bn_IN bn ca cpp cs da de dz el en es fa_AF fa ff fil fr gu ha he hi ht hu id ig is it ja km ko kos mg mi mk ml mn mr ms mvo nb ne nl pa pap pis pl ps pt_BR pt qu ro ru rw sd si sk sl sq sv sw ta te th tpi tr tvl tzo ug ur vi wa yo zh_CN zh_TW"
+
GETTEXT_PACKAGE=sugar-toolkit
AC_PROG_INTLTOOL([0.33])
AC_SUBST(GETTEXT_PACKAGE)
diff --git a/po/de.po b/po/de.po
index 6beb708..5ee3a4d 100644
--- a/po/de.po
+++ b/po/de.po
@@ -1,40 +1,52 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
# Markus Schlager <m.slg@gmx.de>, 2008.
msgid ""
msgstr ""
"Project-Id-Version: sugar-toolkit\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2009-08-26 00:33-0400\n"
-"PO-Revision-Date: 2009-09-23 15:44-0400\n"
-"Last-Translator: Markus Schlager <m.slg@gmx.de>\n"
+"POT-Creation-Date: 2010-01-20 13:10-0500\n"
+"PO-Revision-Date: 2010-03-26 20:39+0200\n"
+"Last-Translator: Markus <m.slg@gmx.de>\n"
"Language-Team: OLPC-German <LL@li.org>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Generator: Pootle 1.2.1\n"
+"X-Generator: Pootle 2.0.3\n"
-#: ../src/sugar/activity/activity.py:329
+#: ../src/sugar/activity/activity.py:338
#, python-format
msgid "%s Activity"
msgstr "%s Aktivität"
-#: ../src/sugar/activity/activity.py:714
+#: ../src/sugar/activity/activity.py:738
msgid "Keep error"
msgstr "Fehler beim Speichern"
-#: ../src/sugar/activity/activity.py:715
+#: ../src/sugar/activity/activity.py:739
msgid "Keep error: all changes will be lost"
msgstr "Fehler beim Speichern: Alle Änderungen gehen verloren"
-#: ../src/sugar/activity/activity.py:718
+#: ../src/sugar/activity/activity.py:742
msgid "Don't stop"
msgstr "Nicht beenden"
-#: ../src/sugar/activity/activity.py:721
+#: ../src/sugar/activity/activity.py:745
msgid "Stop anyway"
msgstr "Trotzdem beenden"
@@ -44,7 +56,7 @@ msgstr "Diesen Eintrag benennen"
# (Markus S.) war 'Behalten'
#: ../src/sugar/activity/namingalert.py:87
-#: ../src/sugar/activity/widgets.py:162
+#: ../src/sugar/activity/widgets.py:166
msgid "Keep"
msgstr "Speichern"
@@ -60,35 +72,35 @@ msgstr "Beschreibung:"
msgid "Tags:"
msgstr "Stichwörter:"
-#: ../src/sugar/activity/widgets.py:79
+#: ../src/sugar/activity/widgets.py:83
msgid "Stop"
msgstr "Beenden"
-#: ../src/sugar/activity/widgets.py:91
+#: ../src/sugar/activity/widgets.py:95
msgid "Undo"
msgstr "Rückgängig"
-#: ../src/sugar/activity/widgets.py:99
+#: ../src/sugar/activity/widgets.py:103
msgid "Redo"
-msgstr "Wiederholen"
+msgstr "Wiederherstellen"
-#: ../src/sugar/activity/widgets.py:106
+#: ../src/sugar/activity/widgets.py:110
msgid "Copy"
msgstr "Kopieren"
-#: ../src/sugar/activity/widgets.py:113
+#: ../src/sugar/activity/widgets.py:117
msgid "Paste"
msgstr "Einfügen"
-#: ../src/sugar/activity/widgets.py:123
+#: ../src/sugar/activity/widgets.py:127
msgid "Private"
msgstr "Privat"
-#: ../src/sugar/activity/widgets.py:130
+#: ../src/sugar/activity/widgets.py:134
msgid "My Neighborhood"
msgstr "Meine Umgebung"
-#: ../src/sugar/activity/widgets.py:341
+#: ../src/sugar/activity/widgets.py:345
msgid "Activity"
msgstr "Aktivität"
@@ -132,7 +144,7 @@ msgstr ", "
#. TRANS: Indicating something that just happened, eg. "just now", "moments ago"
#: ../src/sugar/util.py:222
msgid "Seconds ago"
-msgstr "vor Sekunden"
+msgstr "Gerade eben"
# TRANS: Indicating time passed, eg. "[10 day, 5 hours] ago",
# "[2 minutes] in the past", or "[3 years, 1 month] earlier"
diff --git a/po/es.po b/po/es.po
index b330593..183a54e 100644
--- a/po/es.po
+++ b/po/es.po
@@ -6,140 +6,137 @@ msgid ""
msgstr ""
"Project-Id-Version: olpc-sugar\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2009-01-20 00:31-0500\n"
-"PO-Revision-Date: 2009-01-30 20:27-0500\n"
-"Last-Translator: Maria del Pilar Saenz Rodriguez <mapisaro@gmail.com>\n"
+"POT-Creation-Date: 2010-01-20 13:10-0500\n"
+"PO-Revision-Date: 2010-03-11 23:18+0200\n"
+"Last-Translator: Roger Orellana <rjorellana@gmail.com>\n"
"Language-Team: Fedora Spanish <fedora-trans-es@redhat.com>\n"
+"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Generator: Pootle 1.1.0rc2\n"
+"X-Generator: Pootle 2.0.1\n"
"X-Poedit-Language: Spanish\n"
"X-Poedit-SourceCharset: utf-8\n"
"X-Poedit-Basepath: .\n"
-#: ../src/sugar/activity/activity.py:123
-msgid "Share with:"
-msgstr "Compartir con:"
-
-#: ../src/sugar/activity/activity.py:125
-msgid "Private"
-msgstr "Privado"
-
-#: ../src/sugar/activity/activity.py:126
-msgid "My Neighborhood"
-msgstr "Mi Vecindario"
-
-# self._stop_item = MenuItem(_('Stop download'), 'stock-close')
-# TODO: Implement stopping downloads
-# self._stop_item.connect('activate', self._stop_item_activate_cb)
-# self.append_menu_item(self._stop_item)
-#: ../src/sugar/activity/activity.py:133
-#: ../src/sugar/activity/namingalert.py:65
-msgid "Keep"
-msgstr "Guardar"
-
-#: ../src/sugar/activity/activity.py:144
-msgid "Stop"
-msgstr "Parar"
-
-#: ../src/sugar/activity/activity.py:258
-msgid "Undo"
-msgstr "Deshacer"
-
-#: ../src/sugar/activity/activity.py:263
-msgid "Redo"
-msgstr "Rehacer"
-
-#: ../src/sugar/activity/activity.py:273
-msgid "Copy"
-msgstr "Copiar"
-
-#: ../src/sugar/activity/activity.py:278
-msgid "Paste"
-msgstr "Pegar"
-
-#: ../src/sugar/activity/activity.py:304
-msgid "Activity"
-msgstr "Actividad"
-
-#: ../src/sugar/activity/activity.py:542
+#: ../src/sugar/activity/activity.py:338
#, python-format
msgid "%s Activity"
msgstr "Actividad %s"
-#: ../src/sugar/activity/activity.py:910
+#: ../src/sugar/activity/activity.py:738
msgid "Keep error"
msgstr "Error al guardar"
-#: ../src/sugar/activity/activity.py:911
+#: ../src/sugar/activity/activity.py:739
msgid "Keep error: all changes will be lost"
msgstr "Error al guardar: todos los cambios se perderán"
-#: ../src/sugar/activity/activity.py:914
+#: ../src/sugar/activity/activity.py:742
msgid "Don't stop"
msgstr "No detener"
-#: ../src/sugar/activity/activity.py:917
+#: ../src/sugar/activity/activity.py:745
msgid "Stop anyway"
msgstr "Detener de todas formas"
-#: ../src/sugar/activity/namingalert.py:60
+#: ../src/sugar/activity/namingalert.py:82
msgid "Name this entry"
msgstr "Nombre esta entrada"
-#: ../src/sugar/activity/namingalert.py:248
+# self._stop_item = MenuItem(_('Stop download'), 'stock-close')
+# TODO: Implement stopping downloads
+# self._stop_item.connect('activate', self._stop_item_activate_cb)
+# self.append_menu_item(self._stop_item)
+#: ../src/sugar/activity/namingalert.py:87
+#: ../src/sugar/activity/widgets.py:166
+msgid "Keep"
+msgstr "Guardar"
+
+#: ../src/sugar/activity/namingalert.py:283
msgid "Untitled"
msgstr "Sin título"
-#: ../src/sugar/activity/namingalert.py:255
+#: ../src/sugar/activity/namingalert.py:290
msgid "Description:"
msgstr "Descripción:"
-#: ../src/sugar/activity/namingalert.py:279
+#: ../src/sugar/activity/namingalert.py:314
msgid "Tags:"
msgstr "Etiquetas:"
-#: ../src/sugar/graphics/alert.py:288 ../src/sugar/graphics/alert.py:367
+#: ../src/sugar/activity/widgets.py:83
+msgid "Stop"
+msgstr "Parar"
+
+#: ../src/sugar/activity/widgets.py:95
+msgid "Undo"
+msgstr "Deshacer"
+
+#: ../src/sugar/activity/widgets.py:103
+msgid "Redo"
+msgstr "Rehacer"
+
+#: ../src/sugar/activity/widgets.py:110
+msgid "Copy"
+msgstr "Copiar"
+
+#: ../src/sugar/activity/widgets.py:117
+msgid "Paste"
+msgstr "Pegar"
+
+#: ../src/sugar/activity/widgets.py:127
+msgid "Private"
+msgstr "Privado"
+
+#: ../src/sugar/activity/widgets.py:134
+msgid "My Neighborhood"
+msgstr "Mi Vecindario"
+
+#: ../src/sugar/activity/widgets.py:345
+msgid "Activity"
+msgstr "Actividad"
+
+#: ../src/sugar/graphics/alert.py:286 ../src/sugar/graphics/alert.py:365
msgid "Cancel"
msgstr "Cancelar"
-#: ../src/sugar/graphics/alert.py:292 ../src/sugar/graphics/alert.py:426
+#: ../src/sugar/graphics/alert.py:290 ../src/sugar/graphics/alert.py:424
msgid "Ok"
-msgstr "Ok"
+msgstr "Aceptar"
-#: ../src/sugar/graphics/alert.py:377
+#: ../src/sugar/graphics/alert.py:375
msgid "Continue"
msgstr "Continuar"
-#: ../src/sugar/graphics/colorbutton.py:49
+#: ../src/sugar/graphics/colorbutton.py:52
msgid "Choose a color"
msgstr "Escoja un color"
-#: ../src/sugar/graphics/colorbutton.py:262
+#: ../src/sugar/graphics/colorbutton.py:272
msgid "Red"
msgstr "Rojo"
-#: ../src/sugar/graphics/colorbutton.py:264
+#: ../src/sugar/graphics/colorbutton.py:274
msgid "Green"
msgstr "Verde"
-#: ../src/sugar/graphics/colorbutton.py:266
+#: ../src/sugar/graphics/colorbutton.py:276
msgid "Blue"
msgstr "Azul"
-#: ../src/sugar/util.py:194
+#: ../src/sugar/util.py:218
msgid " and "
msgstr " y "
-#: ../src/sugar/util.py:195
+#: ../src/sugar/util.py:219
msgid ", "
msgstr ", "
# TRANS: Indicating something that just happened, eg. "just now", "moments ago"
#. TRANS: Indicating something that just happened, eg. "just now", "moments ago"
-#: ../src/sugar/util.py:198
+#: ../src/sugar/util.py:222
msgid "Seconds ago"
msgstr "Segundos atrás"
@@ -148,7 +145,7 @@ msgstr "Segundos atrás"
# "[2 minutes] in the past", or "[3 years, 1 month] earlier"
#. TRANS: Indicating time passed, eg. "[10 day, 5 hours] ago",
#. "[2 minutes] in the past", or "[3 years, 1 month] earlier"
-#: ../src/sugar/util.py:202
+#: ../src/sugar/util.py:226
#, python-format
msgid "%s ago"
msgstr "%s atrás"
@@ -157,48 +154,75 @@ msgstr "%s atrás"
# Traduction: I don't know why somebody wrote the same for plural and singular traduction.
# TRANS: Relative dates (eg. 1 month and 5 days).
#. TRANS: Relative dates (eg. 1 month and 5 days).
-#: ../src/sugar/util.py:215
+#: ../src/sugar/util.py:241
#, python-format
msgid "%d year"
msgid_plural "%d years"
msgstr[0] "%d año"
msgstr[1] "%d años"
-#: ../src/sugar/util.py:216
+#: ../src/sugar/util.py:242
#, python-format
msgid "%d month"
msgid_plural "%d months"
msgstr[0] "%d mes"
msgstr[1] "%d meses"
-#: ../src/sugar/util.py:217
+#: ../src/sugar/util.py:243
#, python-format
msgid "%d week"
msgid_plural "%d weeks"
msgstr[0] "%d semana"
msgstr[1] "%d semanas"
-#: ../src/sugar/util.py:218
+#: ../src/sugar/util.py:244
#, python-format
msgid "%d day"
msgid_plural "%d days"
msgstr[0] "%d día"
msgstr[1] "%d días"
-#: ../src/sugar/util.py:219
+#: ../src/sugar/util.py:245
#, python-format
msgid "%d hour"
msgid_plural "%d hours"
msgstr[0] "%d hora"
msgstr[1] "%d horas"
-#: ../src/sugar/util.py:220
+#: ../src/sugar/util.py:246
#, python-format
msgid "%d minute"
msgid_plural "%d minutes"
msgstr[0] "%d minuto"
msgstr[1] "%d minutos"
+#: ../src/sugar/util.py:339
+msgid "Empty"
+msgstr "Vacio"
+
+#: ../src/sugar/util.py:341
+#, python-format
+msgid "%d B"
+msgstr "%d B"
+
+#: ../src/sugar/util.py:343
+#, python-format
+msgid "%d KB"
+msgstr "%d KB"
+
+#: ../src/sugar/util.py:345
+#, python-format
+msgid "%d MB"
+msgstr "%d MB"
+
+#: ../src/sugar/util.py:347
+#, python-format
+msgid "%d GB"
+msgstr "%d GB"
+
+#~ msgid "Share with:"
+#~ msgstr "Compartir con:"
+
#~ msgid "Name:"
#~ msgstr "Nombre:"
diff --git a/po/hi.po b/po/hi.po
index 93a56e3..321676a 100644
--- a/po/hi.po
+++ b/po/hi.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2008-06-24 00:07+0530\n"
+"POT-Creation-Date: 2010-01-20 13:10-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -16,138 +16,191 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Translate Toolkit 1.1.1rc4\n"
-#: ../src/sugar/activity/activity.py:120
-msgid "Share with:"
-msgstr ""
+#: ../src/sugar/activity/activity.py:338
+#, python-format
+msgid "%s Activity"
+msgstr "%s क्रियाएँ"
-#: ../src/sugar/activity/activity.py:122
-msgid "Private"
-msgstr ""
+#: ../src/sugar/activity/activity.py:738
+msgid "Keep error"
+msgstr "त्रुटि को रखे रहें"
-#: ../src/sugar/activity/activity.py:123
-msgid "My Neighborhood"
-msgstr ""
+#: ../src/sugar/activity/activity.py:739
+msgid "Keep error: all changes will be lost"
+msgstr "त्रुटि को रखे रहें: सारे परिवर्तन खो जाएँगे"
+
+#: ../src/sugar/activity/activity.py:742
+msgid "Don't stop"
+msgstr "बन्द न करें"
+
+#: ../src/sugar/activity/activity.py:745
+msgid "Stop anyway"
+msgstr "जैसे भी हो बन्द करें"
+
+#: ../src/sugar/activity/namingalert.py:82
+msgid "Name this entry"
+msgstr "इस प्रविष्टि को नाम दें"
-#: ../src/sugar/activity/activity.py:130
+#: ../src/sugar/activity/namingalert.py:87
+#: ../src/sugar/activity/widgets.py:166
msgid "Keep"
-msgstr ""
+msgstr "रखें"
+
+#: ../src/sugar/activity/namingalert.py:283
+msgid "Untitled"
+msgstr "बिना शीर्षक"
-#: ../src/sugar/activity/activity.py:136
+#: ../src/sugar/activity/namingalert.py:290
+msgid "Description:"
+msgstr "वर्णन:"
+
+#: ../src/sugar/activity/namingalert.py:314
+msgid "Tags:"
+msgstr "टैग्स:"
+
+#: ../src/sugar/activity/widgets.py:83
msgid "Stop"
-msgstr ""
+msgstr "रूकें"
-#: ../src/sugar/activity/activity.py:251
+#: ../src/sugar/activity/widgets.py:95
msgid "Undo"
-msgstr ""
+msgstr "पहले जैसा"
-#: ../src/sugar/activity/activity.py:256
+#: ../src/sugar/activity/widgets.py:103
msgid "Redo"
-msgstr ""
+msgstr "दोहराएँ"
-#: ../src/sugar/activity/activity.py:266
+#: ../src/sugar/activity/widgets.py:110
msgid "Copy"
-msgstr ""
+msgstr "नक़ल"
-#: ../src/sugar/activity/activity.py:271
+#: ../src/sugar/activity/widgets.py:117
msgid "Paste"
-msgstr ""
-
-#: ../src/sugar/activity/activity.py:297
-msgid "Activity"
-msgstr ""
-
-#: ../src/sugar/activity/activity.py:469
-#, python-format
-msgid "%s Activity"
-msgstr ""
-
-#: ../src/sugar/activity/activity.py:856
-msgid "Keep error"
-msgstr ""
+msgstr "चिपकाएँ"
-#: ../src/sugar/activity/activity.py:857
-msgid "Keep error: all changes will be lost"
-msgstr ""
+#: ../src/sugar/activity/widgets.py:127
+msgid "Private"
+msgstr "निजी"
-#: ../src/sugar/activity/activity.py:860
-msgid "Don't stop"
-msgstr ""
+#: ../src/sugar/activity/widgets.py:134
+msgid "My Neighborhood"
+msgstr "मेरा पड़ोस"
-#: ../src/sugar/activity/activity.py:863
-msgid "Stop anyway"
-msgstr ""
+#: ../src/sugar/activity/widgets.py:345
+msgid "Activity"
+msgstr "क्रियाएँ"
-#: ../src/sugar/graphics/alert.py:166 ../src/sugar/graphics/alert.py:209
+#: ../src/sugar/graphics/alert.py:286 ../src/sugar/graphics/alert.py:365
msgid "Cancel"
-msgstr ""
+msgstr "रद्द करें"
-#: ../src/sugar/graphics/alert.py:170 ../src/sugar/graphics/alert.py:247
+#: ../src/sugar/graphics/alert.py:290 ../src/sugar/graphics/alert.py:424
msgid "Ok"
-msgstr ""
+msgstr "ठीक"
-#: ../src/sugar/graphics/alert.py:219
+#: ../src/sugar/graphics/alert.py:375
msgid "Continue"
-msgstr ""
+msgstr "जारी रखें"
+
+#: ../src/sugar/graphics/colorbutton.py:52
+msgid "Choose a color"
+msgstr "रंग चुनें"
+
+#: ../src/sugar/graphics/colorbutton.py:272
+msgid "Red"
+msgstr "लाल"
-#: ../src/sugar/util.py:181
+#: ../src/sugar/graphics/colorbutton.py:274
+msgid "Green"
+msgstr "हरा"
+
+#: ../src/sugar/graphics/colorbutton.py:276
+msgid "Blue"
+msgstr "नीला"
+
+#: ../src/sugar/util.py:218
msgid " and "
-msgstr ""
+msgstr " और"
-#: ../src/sugar/util.py:182
+#: ../src/sugar/util.py:219
msgid ", "
-msgstr ""
+msgstr ","
#. TRANS: Indicating something that just happened, eg. "just now", "moments ago"
-#: ../src/sugar/util.py:185
+#: ../src/sugar/util.py:222
msgid "Seconds ago"
-msgstr ""
+msgstr "सेकण्ड पहले"
#. TRANS: Indicating time passed, eg. "[10 day, 5 hours] ago",
#. "[2 minutes] in the past", or "[3 years, 1 month] earlier"
-#: ../src/sugar/util.py:189
+#: ../src/sugar/util.py:226
#, python-format
msgid "%s ago"
-msgstr ""
+msgstr "%s पहले"
#. TRANS: Relative dates (eg. 1 month and 5 days).
-#: ../src/sugar/util.py:202
+#: ../src/sugar/util.py:241
#, python-format
msgid "%d year"
msgid_plural "%d years"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%d वर्ष"
+msgstr[1] "%d वर्ष"
-#: ../src/sugar/util.py:203
+#: ../src/sugar/util.py:242
#, python-format
msgid "%d month"
msgid_plural "%d months"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%d माह"
+msgstr[1] "%d माह"
-#: ../src/sugar/util.py:204
+#: ../src/sugar/util.py:243
#, python-format
msgid "%d week"
msgid_plural "%d weeks"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%d सप्ताह"
+msgstr[1] "%d सप्ताह"
-#: ../src/sugar/util.py:205
+#: ../src/sugar/util.py:244
#, python-format
msgid "%d day"
msgid_plural "%d days"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%d दिन"
+msgstr[1] "%d दिन"
-#: ../src/sugar/util.py:206
+#: ../src/sugar/util.py:245
#, python-format
msgid "%d hour"
msgid_plural "%d hours"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%d घंटा"
+msgstr[1] "%d घंटा"
-#: ../src/sugar/util.py:207
+#: ../src/sugar/util.py:246
#, python-format
msgid "%d minute"
msgid_plural "%d minutes"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%d मिनट"
+msgstr[1] "%d मिनट"
+
+#: ../src/sugar/util.py:339
+msgid "Empty"
+msgstr "रिक्त"
+
+#: ../src/sugar/util.py:341
+#, python-format
+msgid "%d B"
+msgstr "%d बा."
+
+#: ../src/sugar/util.py:343
+#, python-format
+msgid "%d KB"
+msgstr "%d कि.बा."
+
+#: ../src/sugar/util.py:345
+#, python-format
+msgid "%d MB"
+msgstr "%d मे.बा."
+
+#: ../src/sugar/util.py:347
+#, python-format
+msgid "%d GB"
+msgstr "%d गी.बा."
diff --git a/po/it.po b/po/it.po
index 2f7a6a8..0013608 100644
--- a/po/it.po
+++ b/po/it.po
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2009-08-26 00:33-0400\n"
+"POT-Creation-Date: 2010-01-20 13:10-0500\n"
"PO-Revision-Date: 2009-09-01 12:12-0400\n"
"Last-Translator: Carlo Falciola <cfalciola@yahoo.it>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,24 +17,24 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Pootle 1.2.1\n"
-#: ../src/sugar/activity/activity.py:329
+#: ../src/sugar/activity/activity.py:338
#, python-format
msgid "%s Activity"
msgstr "Attività %s "
-#: ../src/sugar/activity/activity.py:714
+#: ../src/sugar/activity/activity.py:738
msgid "Keep error"
msgstr "Errore di memorizzazione"
-#: ../src/sugar/activity/activity.py:715
+#: ../src/sugar/activity/activity.py:739
msgid "Keep error: all changes will be lost"
msgstr "Errore di memorizzazione: tutte le modifiche saranno perse"
-#: ../src/sugar/activity/activity.py:718
+#: ../src/sugar/activity/activity.py:742
msgid "Don't stop"
msgstr "Non Fermare"
-#: ../src/sugar/activity/activity.py:721
+#: ../src/sugar/activity/activity.py:745
msgid "Stop anyway"
msgstr "Ferma comunque"
@@ -43,7 +43,7 @@ msgid "Name this entry"
msgstr "Dai un nome a questo oggetto"
#: ../src/sugar/activity/namingalert.py:87
-#: ../src/sugar/activity/widgets.py:162
+#: ../src/sugar/activity/widgets.py:166
msgid "Keep"
msgstr "Memorizza"
@@ -59,35 +59,35 @@ msgstr "Descrizione:"
msgid "Tags:"
msgstr "Etichette:"
-#: ../src/sugar/activity/widgets.py:79
+#: ../src/sugar/activity/widgets.py:83
msgid "Stop"
msgstr "Chiudi"
-#: ../src/sugar/activity/widgets.py:91
+#: ../src/sugar/activity/widgets.py:95
msgid "Undo"
msgstr "Annulla"
-#: ../src/sugar/activity/widgets.py:99
+#: ../src/sugar/activity/widgets.py:103
msgid "Redo"
msgstr "Ripeti"
-#: ../src/sugar/activity/widgets.py:106
+#: ../src/sugar/activity/widgets.py:110
msgid "Copy"
msgstr "Copia"
-#: ../src/sugar/activity/widgets.py:113
+#: ../src/sugar/activity/widgets.py:117
msgid "Paste"
msgstr "Incolla"
-#: ../src/sugar/activity/widgets.py:123
+#: ../src/sugar/activity/widgets.py:127
msgid "Private"
msgstr "Privato"
-#: ../src/sugar/activity/widgets.py:130
+#: ../src/sugar/activity/widgets.py:134
msgid "My Neighborhood"
msgstr "I miei vicini"
-#: ../src/sugar/activity/widgets.py:341
+#: ../src/sugar/activity/widgets.py:345
msgid "Activity"
msgstr "Attività"
diff --git a/po/ja.po b/po/ja.po
index 4d07c31..997570c 100644
--- a/po/ja.po
+++ b/po/ja.po
@@ -5,7 +5,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2009-08-26 00:33-0400\n"
+"POT-Creation-Date: 2010-01-20 13:10-0500\n"
"PO-Revision-Date: 2009-09-14 08:00-0400\n"
"Last-Translator: korakurider <korakurider@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -16,24 +16,24 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Pootle 1.2.1\n"
-#: ../src/sugar/activity/activity.py:329
+#: ../src/sugar/activity/activity.py:338
#, python-format
msgid "%s Activity"
msgstr "%s アクティビティ"
-#: ../src/sugar/activity/activity.py:714
+#: ../src/sugar/activity/activity.py:738
msgid "Keep error"
msgstr "保存エラー"
-#: ../src/sugar/activity/activity.py:715
+#: ../src/sugar/activity/activity.py:739
msgid "Keep error: all changes will be lost"
msgstr "保存エラー: 全ての変更は失われてしまいます"
-#: ../src/sugar/activity/activity.py:718
+#: ../src/sugar/activity/activity.py:742
msgid "Don't stop"
msgstr "やめない"
-#: ../src/sugar/activity/activity.py:721
+#: ../src/sugar/activity/activity.py:745
msgid "Stop anyway"
msgstr "とにかくやめる"
@@ -42,7 +42,7 @@ msgid "Name this entry"
msgstr "これに名前をつける"
#: ../src/sugar/activity/namingalert.py:87
-#: ../src/sugar/activity/widgets.py:162
+#: ../src/sugar/activity/widgets.py:166
msgid "Keep"
msgstr "ジャーナルに保存"
@@ -58,35 +58,35 @@ msgstr "説明:"
msgid "Tags:"
msgstr "タグ:"
-#: ../src/sugar/activity/widgets.py:79
+#: ../src/sugar/activity/widgets.py:83
msgid "Stop"
msgstr "停止"
-#: ../src/sugar/activity/widgets.py:91
+#: ../src/sugar/activity/widgets.py:95
msgid "Undo"
msgstr "元に戻す"
-#: ../src/sugar/activity/widgets.py:99
+#: ../src/sugar/activity/widgets.py:103
msgid "Redo"
msgstr "やり直し"
-#: ../src/sugar/activity/widgets.py:106
+#: ../src/sugar/activity/widgets.py:110
msgid "Copy"
msgstr "コピー"
-#: ../src/sugar/activity/widgets.py:113
+#: ../src/sugar/activity/widgets.py:117
msgid "Paste"
msgstr "貼り付け"
-#: ../src/sugar/activity/widgets.py:123
+#: ../src/sugar/activity/widgets.py:127
msgid "Private"
msgstr "(共有しない)"
-#: ../src/sugar/activity/widgets.py:130
+#: ../src/sugar/activity/widgets.py:134
msgid "My Neighborhood"
msgstr "私のお隣さん"
-#: ../src/sugar/activity/widgets.py:341
+#: ../src/sugar/activity/widgets.py:345
msgid "Activity"
msgstr "アクティビティ"
diff --git a/po/mn.po b/po/mn.po
index 5e9e4c2..11acda4 100644
--- a/po/mn.po
+++ b/po/mn.po
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2009-08-26 00:33-0400\n"
+"POT-Creation-Date: 2010-01-20 13:10-0500\n"
"PO-Revision-Date: 2009-09-21 18:10-0400\n"
"Last-Translator: Odontsetseg Bat-Erdene <obat-erdene@suffolk.edu>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,24 +17,24 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Pootle 1.2.1\n"
-#: ../src/sugar/activity/activity.py:329
+#: ../src/sugar/activity/activity.py:338
#, python-format
msgid "%s Activity"
msgstr "%s үйл ажиллагаа"
-#: ../src/sugar/activity/activity.py:714
+#: ../src/sugar/activity/activity.py:738
msgid "Keep error"
msgstr "Хадгалахын алдаа"
-#: ../src/sugar/activity/activity.py:715
+#: ../src/sugar/activity/activity.py:739
msgid "Keep error: all changes will be lost"
msgstr "Хадгалахын алдаа: бүх өөрчлөлтүүд устгагдана"
-#: ../src/sugar/activity/activity.py:718
+#: ../src/sugar/activity/activity.py:742
msgid "Don't stop"
msgstr "Бүү хаа"
-#: ../src/sugar/activity/activity.py:721
+#: ../src/sugar/activity/activity.py:745
msgid "Stop anyway"
msgstr "Ямар ч нөхцөлд хаах"
@@ -43,7 +43,7 @@ msgid "Name this entry"
msgstr ""
#: ../src/sugar/activity/namingalert.py:87
-#: ../src/sugar/activity/widgets.py:162
+#: ../src/sugar/activity/widgets.py:166
msgid "Keep"
msgstr "Хадгалах"
@@ -59,35 +59,35 @@ msgstr ""
msgid "Tags:"
msgstr ""
-#: ../src/sugar/activity/widgets.py:79
+#: ../src/sugar/activity/widgets.py:83
msgid "Stop"
msgstr "Хаах"
-#: ../src/sugar/activity/widgets.py:91
+#: ../src/sugar/activity/widgets.py:95
msgid "Undo"
msgstr "Буцаах"
-#: ../src/sugar/activity/widgets.py:99
+#: ../src/sugar/activity/widgets.py:103
msgid "Redo"
msgstr "Давтах"
-#: ../src/sugar/activity/widgets.py:106
+#: ../src/sugar/activity/widgets.py:110
msgid "Copy"
msgstr "Хуулах"
-#: ../src/sugar/activity/widgets.py:113
+#: ../src/sugar/activity/widgets.py:117
msgid "Paste"
msgstr "Тавих"
-#: ../src/sugar/activity/widgets.py:123
+#: ../src/sugar/activity/widgets.py:127
msgid "Private"
msgstr "Хувийн"
-#: ../src/sugar/activity/widgets.py:130
+#: ../src/sugar/activity/widgets.py:134
msgid "My Neighborhood"
msgstr "Миний Хөршүүд"
-#: ../src/sugar/activity/widgets.py:341
+#: ../src/sugar/activity/widgets.py:345
msgid "Activity"
msgstr "Үйл ажиллагаа"
diff --git a/po/nl.po b/po/nl.po
index fe97897..1b1fc01 100644
--- a/po/nl.po
+++ b/po/nl.po
@@ -2,48 +2,52 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2009-08-26 00:33-0400\n"
-"PO-Revision-Date: 2009-09-05 07:26-0400\n"
-"Last-Translator: Myckel Habets <myckel@sdf.lonestar.org>\n"
+"POT-Creation-Date: 2010-01-20 13:10-0500\n"
+"PO-Revision-Date: 2010-06-28 21:09+0200\n"
+"Last-Translator: whe <heppew@yahoo.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Generator: Pootle 1.2.1\n"
+"X-Generator: Pootle 2.0.3\n"
-#: ../src/sugar/activity/activity.py:329
+#: ../src/sugar/activity/activity.py:338
#, python-format
msgid "%s Activity"
-msgstr "%s activiteit"
+msgstr "%s Activiteit"
-#: ../src/sugar/activity/activity.py:714
+#: ../src/sugar/activity/activity.py:738
msgid "Keep error"
msgstr "Bewaarfout"
-#: ../src/sugar/activity/activity.py:715
+#: ../src/sugar/activity/activity.py:739
msgid "Keep error: all changes will be lost"
msgstr "Bewaarfout: alle veranderingen zijn verloren gegaan"
-#: ../src/sugar/activity/activity.py:718
+#: ../src/sugar/activity/activity.py:742
msgid "Don't stop"
msgstr "Niet stoppen"
-#: ../src/sugar/activity/activity.py:721
+#: ../src/sugar/activity/activity.py:745
msgid "Stop anyway"
msgstr "Toch stoppen"
#: ../src/sugar/activity/namingalert.py:82
msgid "Name this entry"
-msgstr "Benoem deze ingang"
+msgstr "Benoem deze invoer"
#: ../src/sugar/activity/namingalert.py:87
-#: ../src/sugar/activity/widgets.py:162
+#: ../src/sugar/activity/widgets.py:166
msgid "Keep"
msgstr "Behouden"
@@ -59,35 +63,35 @@ msgstr "Beschrijving:"
msgid "Tags:"
msgstr "Labels:"
-#: ../src/sugar/activity/widgets.py:79
+#: ../src/sugar/activity/widgets.py:83
msgid "Stop"
msgstr "Stop"
-#: ../src/sugar/activity/widgets.py:91
+#: ../src/sugar/activity/widgets.py:95
msgid "Undo"
msgstr "Ongedaan maken"
-#: ../src/sugar/activity/widgets.py:99
+#: ../src/sugar/activity/widgets.py:103
msgid "Redo"
msgstr "Herhalen"
-#: ../src/sugar/activity/widgets.py:106
+#: ../src/sugar/activity/widgets.py:110
msgid "Copy"
msgstr "Kopiëren"
-#: ../src/sugar/activity/widgets.py:113
+#: ../src/sugar/activity/widgets.py:117
msgid "Paste"
msgstr "Plakken"
-#: ../src/sugar/activity/widgets.py:123
+#: ../src/sugar/activity/widgets.py:127
msgid "Private"
msgstr "Privé"
-#: ../src/sugar/activity/widgets.py:130
+#: ../src/sugar/activity/widgets.py:134
msgid "My Neighborhood"
msgstr "Mijn omgeving"
-#: ../src/sugar/activity/widgets.py:341
+#: ../src/sugar/activity/widgets.py:345
msgid "Activity"
msgstr "Activiteit"
diff --git a/po/tvl.po b/po/tvl.po
new file mode 100644
index 0000000..5e78660
--- /dev/null
+++ b/po/tvl.po
@@ -0,0 +1,206 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2009-08-26 00:33-0400\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Translate Toolkit 1.3.0\n"
+
+#: ../src/sugar/activity/activity.py:329
+#, python-format
+msgid "%s Activity"
+msgstr ""
+
+#: ../src/sugar/activity/activity.py:714
+msgid "Keep error"
+msgstr ""
+
+#: ../src/sugar/activity/activity.py:715
+msgid "Keep error: all changes will be lost"
+msgstr ""
+
+#: ../src/sugar/activity/activity.py:718
+msgid "Don't stop"
+msgstr ""
+
+#: ../src/sugar/activity/activity.py:721
+msgid "Stop anyway"
+msgstr ""
+
+#: ../src/sugar/activity/namingalert.py:82
+msgid "Name this entry"
+msgstr ""
+
+#: ../src/sugar/activity/namingalert.py:87
+#: ../src/sugar/activity/widgets.py:162
+msgid "Keep"
+msgstr ""
+
+#: ../src/sugar/activity/namingalert.py:283
+msgid "Untitled"
+msgstr ""
+
+#: ../src/sugar/activity/namingalert.py:290
+msgid "Description:"
+msgstr ""
+
+#: ../src/sugar/activity/namingalert.py:314
+msgid "Tags:"
+msgstr ""
+
+#: ../src/sugar/activity/widgets.py:79
+msgid "Stop"
+msgstr ""
+
+#: ../src/sugar/activity/widgets.py:91
+msgid "Undo"
+msgstr ""
+
+#: ../src/sugar/activity/widgets.py:99
+msgid "Redo"
+msgstr ""
+
+#: ../src/sugar/activity/widgets.py:106
+msgid "Copy"
+msgstr ""
+
+#: ../src/sugar/activity/widgets.py:113
+msgid "Paste"
+msgstr ""
+
+#: ../src/sugar/activity/widgets.py:123
+msgid "Private"
+msgstr ""
+
+#: ../src/sugar/activity/widgets.py:130
+msgid "My Neighborhood"
+msgstr ""
+
+#: ../src/sugar/activity/widgets.py:341
+msgid "Activity"
+msgstr ""
+
+#: ../src/sugar/graphics/alert.py:286 ../src/sugar/graphics/alert.py:365
+msgid "Cancel"
+msgstr ""
+
+#: ../src/sugar/graphics/alert.py:290 ../src/sugar/graphics/alert.py:424
+msgid "Ok"
+msgstr ""
+
+#: ../src/sugar/graphics/alert.py:375
+msgid "Continue"
+msgstr ""
+
+#: ../src/sugar/graphics/colorbutton.py:52
+msgid "Choose a color"
+msgstr ""
+
+#: ../src/sugar/graphics/colorbutton.py:272
+msgid "Red"
+msgstr ""
+
+#: ../src/sugar/graphics/colorbutton.py:274
+msgid "Green"
+msgstr ""
+
+#: ../src/sugar/graphics/colorbutton.py:276
+msgid "Blue"
+msgstr ""
+
+#: ../src/sugar/util.py:218
+msgid " and "
+msgstr ""
+
+#: ../src/sugar/util.py:219
+msgid ", "
+msgstr ""
+
+#. TRANS: Indicating something that just happened, eg. "just now", "moments ago"
+#: ../src/sugar/util.py:222
+msgid "Seconds ago"
+msgstr ""
+
+#. TRANS: Indicating time passed, eg. "[10 day, 5 hours] ago",
+#. "[2 minutes] in the past", or "[3 years, 1 month] earlier"
+#: ../src/sugar/util.py:226
+#, python-format
+msgid "%s ago"
+msgstr ""
+
+#. TRANS: Relative dates (eg. 1 month and 5 days).
+#: ../src/sugar/util.py:241
+#, python-format
+msgid "%d year"
+msgid_plural "%d years"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ../src/sugar/util.py:242
+#, python-format
+msgid "%d month"
+msgid_plural "%d months"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ../src/sugar/util.py:243
+#, python-format
+msgid "%d week"
+msgid_plural "%d weeks"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ../src/sugar/util.py:244
+#, python-format
+msgid "%d day"
+msgid_plural "%d days"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ../src/sugar/util.py:245
+#, python-format
+msgid "%d hour"
+msgid_plural "%d hours"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ../src/sugar/util.py:246
+#, python-format
+msgid "%d minute"
+msgid_plural "%d minutes"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ../src/sugar/util.py:339
+msgid "Empty"
+msgstr ""
+
+#: ../src/sugar/util.py:341
+#, python-format
+msgid "%d B"
+msgstr ""
+
+#: ../src/sugar/util.py:343
+#, python-format
+msgid "%d KB"
+msgstr ""
+
+#: ../src/sugar/util.py:345
+#, python-format
+msgid "%d MB"
+msgstr ""
+
+#: ../src/sugar/util.py:347
+#, python-format
+msgid "%d GB"
+msgstr ""
diff --git a/po/vi.po b/po/vi.po
index 06606aa..062173a 100644
--- a/po/vi.po
+++ b/po/vi.po
@@ -5,7 +5,7 @@ msgid ""
msgstr ""
"Project-Id-Version: sugar-toolkit\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2009-08-26 00:33-0400\n"
+"POT-Creation-Date: 2010-01-20 13:10-0500\n"
"PO-Revision-Date: 2009-09-25 09:03-0400\n"
"Last-Translator: Clytie Siddall <clytie@riverland.net.au>\n"
"Language-Team: Vietnamese <vi-VN@googlegroups.com>\n"
@@ -16,24 +16,24 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Pootle 1.2.1\n"
-#: ../src/sugar/activity/activity.py:329
+#: ../src/sugar/activity/activity.py:338
#, python-format
msgid "%s Activity"
msgstr "Hoạt động %s"
-#: ../src/sugar/activity/activity.py:714
+#: ../src/sugar/activity/activity.py:738
msgid "Keep error"
msgstr "Giữ lỗi"
-#: ../src/sugar/activity/activity.py:715
+#: ../src/sugar/activity/activity.py:739
msgid "Keep error: all changes will be lost"
msgstr "Giữ lỗi: tất cả các thay đổi sẽ bị mất"
-#: ../src/sugar/activity/activity.py:718
+#: ../src/sugar/activity/activity.py:742
msgid "Don't stop"
msgstr "Không dừng"
-#: ../src/sugar/activity/activity.py:721
+#: ../src/sugar/activity/activity.py:745
msgid "Stop anyway"
msgstr "Vẫn dừng"
@@ -42,7 +42,7 @@ msgid "Name this entry"
msgstr "Đặt tên mục nhập này"
#: ../src/sugar/activity/namingalert.py:87
-#: ../src/sugar/activity/widgets.py:162
+#: ../src/sugar/activity/widgets.py:166
msgid "Keep"
msgstr "Giữ"
@@ -58,35 +58,35 @@ msgstr "Mô tả:"
msgid "Tags:"
msgstr "Thẻ:"
-#: ../src/sugar/activity/widgets.py:79
+#: ../src/sugar/activity/widgets.py:83
msgid "Stop"
msgstr "Dừng"
-#: ../src/sugar/activity/widgets.py:91
+#: ../src/sugar/activity/widgets.py:95
msgid "Undo"
msgstr "Hủy bước"
-#: ../src/sugar/activity/widgets.py:99
+#: ../src/sugar/activity/widgets.py:103
msgid "Redo"
msgstr "Hoàn lại"
-#: ../src/sugar/activity/widgets.py:106
+#: ../src/sugar/activity/widgets.py:110
msgid "Copy"
msgstr "Chép"
-#: ../src/sugar/activity/widgets.py:113
+#: ../src/sugar/activity/widgets.py:117
msgid "Paste"
msgstr "Dán"
-#: ../src/sugar/activity/widgets.py:123
+#: ../src/sugar/activity/widgets.py:127
msgid "Private"
msgstr "Riêng"
-#: ../src/sugar/activity/widgets.py:130
+#: ../src/sugar/activity/widgets.py:134
msgid "My Neighborhood"
msgstr "Hàng xóm mình"
-#: ../src/sugar/activity/widgets.py:341
+#: ../src/sugar/activity/widgets.py:345
msgid "Activity"
msgstr "Hoạt động"
diff --git a/po/zh_TW.po b/po/zh_TW.po
index 2cacea2..082a720 100644
--- a/po/zh_TW.po
+++ b/po/zh_TW.po
@@ -6,178 +6,203 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2009-01-20 00:31-0500\n"
-"PO-Revision-Date: 2009-01-28 19:22-0500\n"
+"POT-Creation-Date: 2010-01-20 13:10-0500\n"
+"PO-Revision-Date: 2009-12-22 08:05-0400\n"
"Last-Translator: Yuan Chao <yuanchao@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: zh_TW\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Pootle 1.1.0rc2\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Pootle 1.2.1\n"
-#: ../src/sugar/activity/activity.py:123
-msgid "Share with:"
-msgstr "分享給:"
-
-#: ../src/sugar/activity/activity.py:125
-msgid "Private"
-msgstr "私人"
-
-#: ../src/sugar/activity/activity.py:126
-msgid "My Neighborhood"
-msgstr "我的鄰居"
-
-#: ../src/sugar/activity/activity.py:133
-#: ../src/sugar/activity/namingalert.py:65
-msgid "Keep"
-msgstr "保存"
-
-#: ../src/sugar/activity/activity.py:144
-msgid "Stop"
-msgstr "停止"
-
-#: ../src/sugar/activity/activity.py:258
-msgid "Undo"
-msgstr "復原"
-
-#: ../src/sugar/activity/activity.py:263
-msgid "Redo"
-msgstr "取消復原"
-
-#: ../src/sugar/activity/activity.py:273
-msgid "Copy"
-msgstr "複製"
-
-#: ../src/sugar/activity/activity.py:278
-msgid "Paste"
-msgstr "貼上"
-
-#: ../src/sugar/activity/activity.py:304
-msgid "Activity"
-msgstr "活動"
-
-#: ../src/sugar/activity/activity.py:542
+#: ../src/sugar/activity/activity.py:338
#, python-format
msgid "%s Activity"
msgstr "%s 活動"
-#: ../src/sugar/activity/activity.py:910
+#: ../src/sugar/activity/activity.py:738
msgid "Keep error"
msgstr "保存時發生錯誤"
-#: ../src/sugar/activity/activity.py:911
+#: ../src/sugar/activity/activity.py:739
msgid "Keep error: all changes will be lost"
msgstr "保存時發生錯誤:所作的變動將遺失"
-#: ../src/sugar/activity/activity.py:914
+#: ../src/sugar/activity/activity.py:742
msgid "Don't stop"
msgstr "不停止"
-#: ../src/sugar/activity/activity.py:917
+#: ../src/sugar/activity/activity.py:745
msgid "Stop anyway"
msgstr "確定停止"
-#: ../src/sugar/activity/namingalert.py:60
+#: ../src/sugar/activity/namingalert.py:82
msgid "Name this entry"
msgstr "命名"
-#: ../src/sugar/activity/namingalert.py:248
+#: ../src/sugar/activity/namingalert.py:87
+#: ../src/sugar/activity/widgets.py:166
+msgid "Keep"
+msgstr "保存"
+
+#: ../src/sugar/activity/namingalert.py:283
msgid "Untitled"
msgstr "未命名"
-#: ../src/sugar/activity/namingalert.py:255
+#: ../src/sugar/activity/namingalert.py:290
msgid "Description:"
msgstr "描述:"
-#: ../src/sugar/activity/namingalert.py:279
+#: ../src/sugar/activity/namingalert.py:314
msgid "Tags:"
msgstr "標籤:"
-#: ../src/sugar/graphics/alert.py:288 ../src/sugar/graphics/alert.py:367
+#: ../src/sugar/activity/widgets.py:83
+msgid "Stop"
+msgstr "停止"
+
+#: ../src/sugar/activity/widgets.py:95
+msgid "Undo"
+msgstr "復原"
+
+#: ../src/sugar/activity/widgets.py:103
+msgid "Redo"
+msgstr "取消復原"
+
+#: ../src/sugar/activity/widgets.py:110
+msgid "Copy"
+msgstr "複製"
+
+#: ../src/sugar/activity/widgets.py:117
+msgid "Paste"
+msgstr "貼上"
+
+#: ../src/sugar/activity/widgets.py:127
+msgid "Private"
+msgstr "私人"
+
+#: ../src/sugar/activity/widgets.py:134
+msgid "My Neighborhood"
+msgstr "我的鄰居"
+
+#: ../src/sugar/activity/widgets.py:345
+msgid "Activity"
+msgstr "活動"
+
+#: ../src/sugar/graphics/alert.py:286 ../src/sugar/graphics/alert.py:365
msgid "Cancel"
msgstr "取消"
-#: ../src/sugar/graphics/alert.py:292 ../src/sugar/graphics/alert.py:426
+#: ../src/sugar/graphics/alert.py:290 ../src/sugar/graphics/alert.py:424
msgid "Ok"
msgstr "確定"
-#: ../src/sugar/graphics/alert.py:377
+#: ../src/sugar/graphics/alert.py:375
msgid "Continue"
msgstr "繼續"
-#: ../src/sugar/graphics/colorbutton.py:49
+#: ../src/sugar/graphics/colorbutton.py:52
msgid "Choose a color"
msgstr "選擇喜歡的顏色"
-#: ../src/sugar/graphics/colorbutton.py:262
+#: ../src/sugar/graphics/colorbutton.py:272
msgid "Red"
msgstr "紅色"
-#: ../src/sugar/graphics/colorbutton.py:264
+#: ../src/sugar/graphics/colorbutton.py:274
msgid "Green"
msgstr "綠色"
-#: ../src/sugar/graphics/colorbutton.py:266
+#: ../src/sugar/graphics/colorbutton.py:276
msgid "Blue"
msgstr "藍色"
-#: ../src/sugar/util.py:194
+#: ../src/sugar/util.py:218
msgid " and "
msgstr " 和 "
-#: ../src/sugar/util.py:195
+#: ../src/sugar/util.py:219
msgid ", "
msgstr ", "
# TRANS: Indicating something that just happened, eg. "just now", "moments ago"
#. TRANS: Indicating something that just happened, eg. "just now", "moments ago"
-#: ../src/sugar/util.py:198
+#: ../src/sugar/util.py:222
msgid "Seconds ago"
-msgstr "秒鐘前"
+msgstr "數秒鐘前"
# TRANS: Indicating time passed, eg. "[10 day, 5 hours] ago",
# "[2 minutes] in the past", or "[3 years, 1 month] earlier"
#. TRANS: Indicating time passed, eg. "[10 day, 5 hours] ago",
#. "[2 minutes] in the past", or "[3 years, 1 month] earlier"
-#: ../src/sugar/util.py:202
+#: ../src/sugar/util.py:226
#, python-format
msgid "%s ago"
msgstr "%s 前"
# TRANS: Relative dates (eg. 1 month and 5 days).
#. TRANS: Relative dates (eg. 1 month and 5 days).
-#: ../src/sugar/util.py:215
+#: ../src/sugar/util.py:241
#, python-format
msgid "%d year"
msgid_plural "%d years"
msgstr[0] "%d 年"
-#: ../src/sugar/util.py:216
+#: ../src/sugar/util.py:242
#, python-format
msgid "%d month"
msgid_plural "%d months"
msgstr[0] "%d 個月"
-#: ../src/sugar/util.py:217
+#: ../src/sugar/util.py:243
#, python-format
msgid "%d week"
msgid_plural "%d weeks"
msgstr[0] "%d 週"
-#: ../src/sugar/util.py:218
+#: ../src/sugar/util.py:244
#, python-format
msgid "%d day"
msgid_plural "%d days"
msgstr[0] "%d 天"
-#: ../src/sugar/util.py:219
+#: ../src/sugar/util.py:245
#, python-format
msgid "%d hour"
msgid_plural "%d hours"
msgstr[0] "%d 小時"
-#: ../src/sugar/util.py:220
+#: ../src/sugar/util.py:246
#, python-format
msgid "%d minute"
msgid_plural "%d minutes"
msgstr[0] "%d 分鐘"
+
+#: ../src/sugar/util.py:339
+msgid "Empty"
+msgstr "無"
+
+#: ../src/sugar/util.py:341
+#, python-format
+msgid "%d B"
+msgstr "%d B"
+
+#: ../src/sugar/util.py:343
+#, python-format
+msgid "%d KB"
+msgstr "%d KB"
+
+#: ../src/sugar/util.py:345
+#, python-format
+msgid "%d MB"
+msgstr "%d MB"
+
+#: ../src/sugar/util.py:347
+#, python-format
+msgid "%d GB"
+msgstr "%d GB"
+
+#~ msgid "Share with:"
+#~ msgstr "分享給:"
diff --git a/src/sugar/__init__.py b/src/sugar/__init__.py
deleted file mode 100644
index 44acb4d..0000000
--- a/src/sugar/__init__.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2 of the License, or (at your option) any later version.
-#
-# This library 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
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library; if not, write to the
-# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
-# Boston, MA 02111-1307, USA.
diff --git a/src/sugar/activity/Makefile.am b/src/sugar/activity/Makefile.am
index e2e6fdc..2c2eff1 100644
--- a/src/sugar/activity/Makefile.am
+++ b/src/sugar/activity/Makefile.am
@@ -6,6 +6,7 @@ sugar_PYTHON = \
activityhandle.py \
activityservice.py \
bundlebuilder.py \
+ i18n.py \
main.py \
namingalert.py \
widgets.py
diff --git a/src/sugar/activity/__init__py b/src/sugar/activity/__init__py
deleted file mode 100644
index e69de29..0000000
--- a/src/sugar/activity/__init__py
+++ /dev/null
diff --git a/src/sugar/activity/activity.py b/src/sugar/activity/activity.py
index d4892d0..0bda2ea 100644
--- a/src/sugar/activity/activity.py
+++ b/src/sugar/activity/activity.py
@@ -31,6 +31,7 @@ STABLE.
"""
# Copyright (C) 2006-2007 Red Hat, Inc.
# Copyright (C) 2007-2009 One Laptop Per Child
+# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -52,14 +53,21 @@ import logging
import os
import time
from hashlib import sha1
-import traceback
-import gconf
+from functools import partial
+import gconf
import gtk
import gobject
import dbus
import dbus.service
+from dbus import PROPERTIES_IFACE
import cjson
+from telepathy.server import DBusProperties
+from telepathy.interfaces import CHANNEL, \
+ CHANNEL_TYPE_TEXT, \
+ CLIENT, \
+ CLIENT_HANDLER
+from telepathy.constants import CONNECTION_HANDLE_TYPE_CONTACT
from sugar import util
from sugar.presence import presenceservice
@@ -88,6 +96,7 @@ J_DBUS_SERVICE = 'org.laptop.Journal'
J_DBUS_PATH = '/org/laptop/Journal'
J_DBUS_INTERFACE = 'org.laptop.Journal'
+CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties'
class _ActivitySession(gobject.GObject):
@@ -267,9 +276,7 @@ class Activity(Window, gtk.Container):
self._active = False
self._activity_id = handle.activity_id
- self._pservice = presenceservice.get_instance()
self.shared_activity = None
- self._share_id = None
self._join_id = None
self._updating_jobject = False
self._closing = False
@@ -302,9 +309,58 @@ class Activity(Window, gtk.Container):
if self._jobject.metadata.has_key('share-scope'):
share_scope = self._jobject.metadata['share-scope']
+ self.shared_activity = None
+ self._join_id = None
+
+ if handle.invited:
+ wait_loop = gobject.MainLoop()
+ self._client_handler = _ClientHandler(
+ self.get_bundle_id(),
+ partial(self.__got_channel_cb, wait_loop))
+ # FIXME: The current API requires that self.shared_activity is set
+ # before exiting from __init__, so we wait until we have got the
+ # shared activity. http://bugs.sugarlabs.org/ticket/2168
+ wait_loop.run()
+ else:
+ pservice = presenceservice.get_instance()
+ mesh_instance = pservice.get_activity(self._activity_id,
+ warn_if_none=False)
+ self._set_up_sharing(mesh_instance, share_scope)
+
+ if handle.object_id is None and create_jobject:
+ logging.debug('Creating a jobject.')
+ self._jobject = self._initialize_journal_object()
+ self.set_title(self._jobject.metadata['title'])
+
+ def _initialize_journal_object(self):
+ title = _('%s Activity') % get_bundle_name()
+
+ if self.shared_activity is not None:
+ icon_color = self.shared_activity.props.color
+ else:
+ client = gconf.client_get_default()
+ icon_color = client.get_string('/desktop/sugar/user/color')
+
+ jobject = datastore.create()
+ jobject.metadata['title'] = title
+ jobject.metadata['title_set_by_user'] = '0'
+ jobject.metadata['activity'] = self.get_bundle_id()
+ jobject.metadata['activity_id'] = self.get_id()
+ jobject.metadata['keep'] = '0'
+ jobject.metadata['preview'] = ''
+ jobject.metadata['share-scope'] = SCOPE_PRIVATE
+ jobject.metadata['icon-color'] = icon_color
+ jobject.file_path = ''
+
+ # FIXME: We should be able to get an ID synchronously from the DS,
+ # then call async the actual create.
+ # http://bugs.sugarlabs.org/ticket/2169
+ datastore.write(jobject)
+
+ return jobject
+
+ def _set_up_sharing(self, mesh_instance, share_scope):
# handle activity share/join
- mesh_instance = self._pservice.get_activity(self._activity_id,
- warn_if_none=False)
logging.debug("*** Act %s, mesh instance %r, scope %s",
self._activity_id, mesh_instance, share_scope)
if mesh_instance is not None:
@@ -332,29 +388,19 @@ class Activity(Window, gtk.Container):
else:
logging.debug('Unknown share scope %r', share_scope)
- if handle.object_id is None and create_jobject:
- logging.debug('Creating a jobject.')
- self._jobject = datastore.create()
- title = _('%s Activity') % get_bundle_name()
- self._jobject.metadata['title'] = title
- self.set_title(self._jobject.metadata['title'])
- self._jobject.metadata['title_set_by_user'] = '0'
- self._jobject.metadata['activity'] = self.get_bundle_id()
- self._jobject.metadata['activity_id'] = self.get_id()
- self._jobject.metadata['keep'] = '0'
- self._jobject.metadata['preview'] = ''
- self._jobject.metadata['share-scope'] = SCOPE_PRIVATE
- if self.shared_activity is not None:
- icon_color = self.shared_activity.props.color
- else:
- client = gconf.client_get_default()
- icon_color = client.get_string('/desktop/sugar/user/color')
- self._jobject.metadata['icon-color'] = icon_color
+ def __got_channel_cb(self, wait_loop, connection_path, channel_path):
+ logging.debug('Activity.__got_channel_cb')
+ connection_name = connection_path.replace('/', '.')[1:]
- self._jobject.file_path = ''
- # Cannot call datastore.write async for creates:
- # https://dev.laptop.org/ticket/3071
- datastore.write(self._jobject)
+ bus = dbus.SessionBus()
+ channel = bus.get_object(connection_name, channel_path)
+ room_handle = channel.Get(CHANNEL, 'TargetHandle')
+
+ pservice = presenceservice.get_instance()
+ mesh_instance = pservice.get_activity_by_handle(connection_path,
+ room_handle)
+ self._set_up_sharing(mesh_instance, SCOPE_PRIVATE)
+ wait_loop.quit()
def get_active(self):
return self._active
@@ -393,6 +439,9 @@ class Activity(Window, gtk.Container):
"""Returns the bundle_id from the activity.info file"""
return os.environ['SUGAR_BUNDLE_ID']
+ def get_canvas(self):
+ return Window.get_canvas(self)
+
def set_canvas(self, canvas):
"""Sets the 'work area' of your activity with the canvas of your
choice.
@@ -403,6 +452,8 @@ class Activity(Window, gtk.Container):
if not self._read_file_called:
canvas.connect('map', self.__canvas_map_cb)
+ canvas = property(get_canvas, set_canvas)
+
def __screen_size_changed_cb(self, screen):
self._adapt_window_to_screen()
@@ -517,7 +568,7 @@ class Activity(Window, gtk.Container):
if self._closing:
self._show_keep_failed_dialog()
self._closing = False
- logging.debug('Error saving activity object to datastore: %s', err)
+ raise RuntimeError('Error saving activity object to datastore: %s', err)
def _cleanup_jobject(self):
if self._jobject:
@@ -634,6 +685,7 @@ class Activity(Window, gtk.Container):
self._jobject.object_id = None
def __privacy_changed_cb(self, shared_activity, param_spec):
+ logging.debug('__privacy_changed_cb %r', shared_activity.props.private)
if shared_activity.props.private:
self._jobject.metadata['share-scope'] = SCOPE_INVITE_ONLY
else:
@@ -641,6 +693,7 @@ class Activity(Window, gtk.Container):
def __joined_cb(self, activity, success, err):
"""Callback when join has finished"""
+ logging.debug('Activity.__joined_cb %r', success)
self.shared_activity.disconnect(self._join_id)
self._join_id = None
if not success:
@@ -665,8 +718,6 @@ class Activity(Window, gtk.Container):
return self.shared_activity.props.joined
def __share_cb(self, ps, success, activity, err):
- self._pservice.disconnect(self._share_id)
- self._share_id = None
if not success:
logging.debug('Share of activity %s failed: %s.',
self._activity_id, err)
@@ -691,22 +742,24 @@ class Activity(Window, gtk.Container):
def _send_invites(self):
while self._invites_queue:
- buddy_key = self._invites_queue.pop()
- buddy = self._pservice.get_buddy(buddy_key)
+ account_path, contact_id = self._invites_queue.pop()
+ pservice = presenceservice.get_instance()
+ buddy = pservice.get_buddy(account_path, contact_id)
if buddy:
self.shared_activity.invite(
buddy, '', self._invite_response_cb)
else:
- logging.error('Cannot invite %s, no such buddy.', buddy_key)
+ logging.error('Cannot invite %s %s, no such buddy',
+ account_path, contact_id)
- def invite(self, buddy_key):
+ def invite(self, account_path, contact_id):
"""Invite a buddy to join this Activity.
Side Effects:
Calls self.share(True) to privately share the activity if it wasn't
shared before.
"""
- self._invites_queue.append(buddy_key)
+ self._invites_queue.append((account_path, contact_id))
if (self.shared_activity is None
or not self.shared_activity.props.joined):
@@ -729,9 +782,9 @@ class Activity(Window, gtk.Container):
verb = private and 'private' or 'public'
logging.debug('Requesting %s share of activity %s.', verb,
self._activity_id)
- self._share_id = self._pservice.connect("activity-shared",
- self.__share_cb)
- self._pservice.share_activity(self, private=private)
+ pservice = presenceservice.get_instance()
+ pservice.connect('activity-shared', self.__share_cb)
+ pservice.share_activity(self, private=private)
def _show_keep_failed_dialog(self):
alert = Alert()
@@ -769,7 +822,7 @@ class Activity(Window, gtk.Container):
try:
self.save()
except:
- logging.info(traceback.format_exc())
+ logging.exception('Error saving activity object to datastore')
self._show_keep_failed_dialog()
return False
@@ -800,7 +853,8 @@ class Activity(Window, gtk.Container):
if not self.can_close():
return
- if skip_save or self.metadata.get('title_set_by_user', '0') == '1':
+ if skip_save or self._jobject is None or \
+ self.metadata.get('title_set_by_user', '0') == '1':
if not self._closing:
if not self._prepare_close(skip_save):
return
@@ -850,6 +904,50 @@ class Activity(Window, gtk.Container):
_shared_activity = property(lambda self: self.shared_activity, None)
+class _ClientHandler(dbus.service.Object, DBusProperties):
+ def __init__(self, bundle_id, got_channel_cb):
+ self._interfaces = set([CLIENT, CLIENT_HANDLER, PROPERTIES_IFACE])
+ self._got_channel_cb = got_channel_cb
+
+ bus = dbus.Bus()
+ name = CLIENT + '.' + bundle_id
+ bus_name = dbus.service.BusName(name, bus=bus)
+
+ path = '/' + name.replace('.', '/')
+ dbus.service.Object.__init__(self, bus_name, path)
+ DBusProperties.__init__(self)
+
+ self._implement_property_get(CLIENT, {
+ 'Interfaces': lambda: list(self._interfaces),
+ })
+ self._implement_property_get(CLIENT_HANDLER, {
+ 'HandlerChannelFilter': self.__get_filters_cb,
+ })
+
+ def __get_filters_cb(self):
+ logging.debug('__get_filters_cb')
+ filters = {
+ CHANNEL + '.ChannelType' : CHANNEL_TYPE_TEXT,
+ CHANNEL + '.TargetHandleType': CONNECTION_HANDLE_TYPE_CONTACT,
+ }
+ filter_dict = dbus.Dictionary(filters, signature='sv')
+ logging.debug('__get_filters_cb %r', dbus.Array([filter_dict],
+ signature='a{sv}'))
+ return dbus.Array([filter_dict], signature='a{sv}')
+
+ @dbus.service.method(dbus_interface=CLIENT_HANDLER,
+ in_signature='ooa(oa{sv})aota{sv}', out_signature='')
+ def HandleChannels(self, account, connection, channels, requests_satisfied,
+ user_action_time, handler_info):
+ logging.debug('HandleChannels\n\t%r\n\t%r\n\t%r\n\t%r\n\t%r\n\t%r',
+ account, connection, channels, requests_satisfied,
+ user_action_time, handler_info)
+ try:
+ for channel in channels:
+ self._got_channel_cb(connection, channel[0])
+ except Exception, e:
+ logging.exception(e)
+
_session = None
diff --git a/src/sugar/activity/activityfactory.py b/src/sugar/activity/activityfactory.py
index c195572..46dc346 100644
--- a/src/sugar/activity/activityfactory.py
+++ b/src/sugar/activity/activityfactory.py
@@ -1,4 +1,5 @@
# Copyright (C) 2006-2007 Red Hat, Inc.
+# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -22,14 +23,15 @@ the moment there is no reason to stabilize this API.
"""
import logging
+import uuid
import dbus
import gobject
-from sugar.presence import presenceservice
from sugar.activity.activityhandle import ActivityHandle
from sugar import util
from sugar import env
+from sugar.datastore import datastore
from errno import EEXIST, ENOSPC
@@ -42,10 +44,6 @@ _SHELL_SERVICE = "org.laptop.Shell"
_SHELL_PATH = "/org/laptop/Shell"
_SHELL_IFACE = "org.laptop.Shell"
-_DS_SERVICE = "org.laptop.sugar.DataStore"
-_DS_INTERFACE = "org.laptop.sugar.DataStore"
-_DS_PATH = "/org/laptop/sugar/DataStore"
-
_ACTIVITY_FACTORY_INTERFACE = "org.laptop.ActivityFactory"
# helper method to close all filedescriptors
@@ -67,25 +65,7 @@ def _close_fds():
def create_activity_id():
"""Generate a new, unique ID for this activity"""
- pservice = presenceservice.get_instance()
-
- # create a new unique activity ID
- i = 0
- act_id = None
- while i < 10:
- act_id = util.unique_id()
- i += 1
-
- # check through network activities
- found = False
- activities = pservice.get_activities()
- for act in activities:
- if act_id == act.props.id:
- found = True
- break
- if not found:
- return act_id
- raise RuntimeError("Cannot generate unique activity id.")
+ return util.unique_id(uuid.getnode())
def get_environment(activity):
@@ -121,7 +101,8 @@ def get_environment(activity):
return environ
-def get_command(activity, activity_id=None, object_id=None, uri=None):
+def get_command(activity, activity_id=None, object_id=None, uri=None,
+ activity_invite=False):
if not activity_id:
activity_id = create_activity_id()
@@ -133,6 +114,8 @@ def get_command(activity, activity_id=None, object_id=None, uri=None):
command.extend(['-o', object_id])
if uri is not None:
command.extend(['-u', uri])
+ if activity_invite:
+ command.append('-i')
# if the command is in $BUNDLE_ROOT/bin, execute the absolute path so there
# is no need to mangle with the shell's PATH
@@ -152,8 +135,7 @@ def open_log_file(activity):
while True:
path = env.get_logs_path('%s-%s.log' % (activity.get_bundle_id(), i))
try:
- fd = os.open(path, os.O_EXCL | os.O_CREAT \
- | os.O_SYNC | os.O_WRONLY, 0644)
+ fd = os.open(path, os.O_EXCL | os.O_CREAT | os.O_WRONLY, 0644)
f = os.fdopen(fd, 'w', 0)
return (path, f)
except OSError, e:
@@ -211,13 +193,9 @@ class ActivityCreationHandler(gobject.GObject):
self._shell = dbus.Interface(bus_object, _SHELL_IFACE)
if handle.activity_id is not None and handle.object_id is None:
- datastore = dbus.Interface(
- bus.get_object(_DS_SERVICE, _DS_PATH), _DS_INTERFACE)
datastore.find({'activity_id': self._handle.activity_id},
- [],
reply_handler=self._find_object_reply_handler,
- error_handler=self._find_object_error_handler,
- byte_arrays=True)
+ error_handler=self._find_object_error_handler)
else:
self._launch_activity()
@@ -241,8 +219,8 @@ class ActivityCreationHandler(gobject.GObject):
environ = get_environment(self._bundle)
(log_path, log_file) = open_log_file(self._bundle)
command = get_command(self._bundle, self._handle.activity_id,
- self._handle.object_id,
- self._handle.uri)
+ self._handle.object_id, self._handle.uri,
+ self._handle.invited)
dev_null = file('/dev/null', 'w')
environment_dir = None
@@ -282,7 +260,8 @@ class ActivityCreationHandler(gobject.GObject):
gobject.child_watch_add(child.pid,
_child_watch_cb,
- (environment_dir, log_file))
+ (environment_dir, log_file,
+ self._handle.activity_id))
def _no_reply_handler(self, *args):
pass
@@ -345,12 +324,25 @@ def create_with_object_id(bundle, object_id):
def _child_watch_cb(pid, condition, user_data):
# FIXME we use standalone method here instead of ActivityCreationHandler's
# member to have workaround code, see #1123
- environment_dir, log_file = user_data
+ environment_dir, log_file, activity_id = user_data
if environment_dir is not None:
subprocess.call(['/bin/rm', '-rf', environment_dir])
+
+ if os.WIFEXITED(condition):
+ status = os.WEXITSTATUS(condition)
+ signum = None
+ message = 'Exited with status %s' % status
+ elif os.WIFSIGNALED(condition):
+ status = None
+ signum = os.WTERMSIG(condition)
+ message = 'Terminated by signal %s' % signum
+ else:
+ status = None
+ signum = os.WTERMSIG(condition)
+ message = 'Undefined status with signal %s' % signum
+
try:
- log_file.write('Activity died: pid %s condition %s data %s\n' %
- (pid, condition, user_data))
+ log_file.write('%s, pid %s data %s\n' % (message, pid, user_data))
finally:
log_file.close()
@@ -360,3 +352,23 @@ def _child_watch_cb(pid, condition, user_data):
except OSError:
# SIGCHLD = SIG_IGN, no zombies
pass
+
+ if status or signum:
+ # XXX have to recreate dbus object since we can't reuse
+ # ActivityCreationHandler's one, see
+ # https://bugs.freedesktop.org/show_bug.cgi?id=23507
+ bus = dbus.SessionBus()
+ bus_object = bus.get_object(_SHELL_SERVICE, _SHELL_PATH)
+ shell = dbus.Interface(bus_object, _SHELL_IFACE)
+
+ def reply_handler_cb(*args):
+ pass
+
+ def error_handler_cb(error):
+ logging.error('Cannot send NotifyLaunchFailure to the shell')
+
+ # TODO send launching failure but activity could already show
+ # main window, see http://bugs.sugarlabs.org/ticket/1447#comment:19
+ shell.NotifyLaunchFailure(activity_id,
+ reply_handler=reply_handler_cb,
+ error_handler=error_handler_cb)
diff --git a/src/sugar/activity/activityhandle.py b/src/sugar/activity/activityhandle.py
index 4ceadb0..4aeac71 100644
--- a/src/sugar/activity/activityhandle.py
+++ b/src/sugar/activity/activityhandle.py
@@ -23,7 +23,8 @@ STABLE.
class ActivityHandle(object):
"""Data structure storing simple activity metadata"""
- def __init__(self, activity_id=None, object_id=None, uri=None):
+ def __init__(self, activity_id=None, object_id=None, uri=None,
+ invited=False):
"""Initialise the handle from activity_id
activity_id -- unique id for the activity to be
@@ -45,14 +46,18 @@ class ActivityHandle(object):
activity, rather than a journal object
(downloads stored on the file system for
example or web pages)
+ invited -- the activity is being launched for handling an invite
+ from the network
"""
self.activity_id = activity_id
self.object_id = object_id
self.uri = uri
+ self.invited = invited
def get_dict(self):
"""Retrieve our settings as a dictionary"""
- result = {'activity_id': self.activity_id}
+ result = {'activity_id': self.activity_id,
+ 'invited': self.invited}
if self.object_id:
result['object_id'] = self.object_id
if self.uri:
@@ -65,5 +70,6 @@ def create_from_dict(handle_dict):
"""Create a handle from a dictionary of parameters"""
result = ActivityHandle(handle_dict['activity_id'],
object_id = handle_dict.get('object_id'),
- uri = handle_dict.get('uri'))
+ uri = handle_dict.get('uri'),
+ invited = handle_dict.get('invited'))
return result
diff --git a/src/sugar/activity/activityservice.py b/src/sugar/activity/activityservice.py
index 36f485c..ff15471 100644
--- a/src/sugar/activity/activityservice.py
+++ b/src/sugar/activity/activityservice.py
@@ -67,8 +67,8 @@ class ActivityService(dbus.service.Object):
self._activity.props.active = active
@dbus.service.method(_ACTIVITY_INTERFACE)
- def Invite(self, buddy_key):
- self._activity.invite(buddy_key)
+ def InviteContact(self, account_path, contact_id):
+ self._activity.invite(account_path, contact_id)
@dbus.service.method(_ACTIVITY_INTERFACE)
def HandleViewSource(self):
diff --git a/src/sugar/activity/bundlebuilder.py b/src/sugar/activity/bundlebuilder.py
index 868ca3d..fc8ebc8 100644
--- a/src/sugar/activity/bundlebuilder.py
+++ b/src/sugar/activity/bundlebuilder.py
@@ -82,7 +82,7 @@ class Config(object):
def update(self):
self.bundle = bundle = ActivityBundle(self.source_dir)
self.version = bundle.get_activity_version()
- self.activity_name = bundle.get_name()
+ self.activity_name = bundle.get_bundle_name()
self.bundle_id = bundle.get_bundle_id()
self.bundle_name = reduce(lambda x, y: x+y, self.activity_name.split())
self.bundle_root_dir = self.bundle_name + '.activity'
diff --git a/src/sugar/activity/i18n.py b/src/sugar/activity/i18n.py
new file mode 100644
index 0000000..1c3c893
--- /dev/null
+++ b/src/sugar/activity/i18n.py
@@ -0,0 +1,144 @@
+# Copyright (C) 2010 One Laptop Per Child
+#
+# Author: Sayamindu Dasgupta <sayamindu@laptop.org>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+import gconf
+
+import locale
+import os
+import struct
+import sys
+
+import dateutil.parser
+import time
+
+_MO_BIG_ENDIAN = 0xde120495
+_MO_LITTLE_ENDIAN = 0x950412de
+
+
+def _read_bin(handle, format_string, byte_count):
+ read_bytes = handle.read(byte_count)
+ return_value = struct.unpack(format_string, read_bytes)
+ if len(return_value) == 1:
+ return return_value[0]
+ else:
+ return return_value
+
+
+def _extract_header(file_path):
+ header = ''
+ handle = open(file_path, 'rb')
+ magic_number = _read_bin(handle, '<I', 4)
+
+ if magic_number == _MO_BIG_ENDIAN:
+ format_string = '>II'
+ elif magic_number == _MO_LITTLE_ENDIAN:
+ format_string = '<II'
+ else:
+ raise IOError('File does not seem to be a valid MO file')
+
+ version_, num_of_strings = _read_bin(handle, format_string, 8)
+
+ msgids_hash_offset, msgstrs_hash_offset = _read_bin(handle, \
+ format_string, 8)
+ handle.seek(msgids_hash_offset)
+
+ msgids_index = []
+ for i in range(num_of_strings):
+ msgids_index.append(_read_bin(handle, format_string, 8))
+ handle.seek(msgstrs_hash_offset)
+
+ msgstrs_index = []
+ for i in range(num_of_strings):
+ msgstrs_index.append(_read_bin(handle, format_string, 8))
+
+ for i in range(num_of_strings):
+ handle.seek(msgids_index[i][1])
+ msgid = handle.read(msgids_index[i][0])
+ if msgid == '':
+ handle.seek(msgstrs_index[i][1])
+ msgstr = handle.read(msgstrs_index[i][0])
+ header = msgstr
+ break
+ else:
+ continue
+
+ handle.close()
+ return header
+
+
+def _extract_modification_time(file_path):
+ header = _extract_header(file_path)
+ items = header.split('\n')
+ for item in items:
+ if item.startswith('PO-Revision-Date:'):
+ time_str = item.split(': ')[1]
+ parsed_time = dateutil.parser.parse(time_str)
+ return time.mktime(parsed_time.timetuple())
+
+ raise ValueError('Could not find a revision date')
+
+
+def get_locale_path(bundle_id):
+ """ Returns the locale path, which is the directory where the preferred
+ MO file is located.
+
+ The preferred MO file is the one with the latest translation.
+
+ @type bundle_id: string
+ @param bundle_id: The bundle id of the activity in question
+ @rtype: string
+ @return: the preferred locale path
+ """
+
+ # Note: We pre-assign weights to the directories so that if no translations
+ # exist, the appropriate fallbacks (eg: bn for bn_BD) can be loaded
+ # The directory with the highest weight is returned, and if a MO file is
+ # found, the weight of the directory is set to the MO's modification time
+ # (as described in the MO header, and _not_ the filesystem mtime)
+
+ candidate_dirs = {}
+
+ if 'SUGAR_LOCALEDIR' in os.environ:
+ candidate_dirs[os.environ['SUGAR_LOCALEDIR']] = 2
+
+ gconf_client = gconf.client_get_default()
+ package_dir = gconf_client.get_string("/desktop/sugar/i18n/langpackdir")
+ if package_dir is not None and package_dir is not '':
+ candidate_dirs[package_dir] = 1
+
+ candidate_dirs[os.path.join(sys.prefix, 'share', 'locale')] = 0
+
+ for candidate_dir in candidate_dirs.keys():
+ if os.path.exists(candidate_dir):
+ full_path = os.path.join(candidate_dir, \
+ locale.getdefaultlocale()[0], 'LC_MESSAGES', \
+ bundle_id + '.mo')
+ if os.path.exists(full_path):
+ try:
+ candidate_dirs[candidate_dir] = \
+ _extract_modification_time(full_path)
+ except (IOError, ValueError):
+ # The mo file is damaged or has not been initialized
+ # Set lowest priority
+ candidate_dirs[candidate_dir] = -1
+
+ available_paths = sorted(candidate_dirs.iteritems(), key=lambda (k, v): \
+ (v, k), reverse=True)
+ preferred_path = available_paths[0][0]
+ return preferred_path
diff --git a/src/sugar/activity/main.py b/src/sugar/activity/main.py
index ef4d001..3a3950d 100644
--- a/src/sugar/activity/main.py
+++ b/src/sugar/activity/main.py
@@ -27,6 +27,7 @@ import dbus.glib
import sugar
from sugar.activity import activityhandle
+from sugar.activity import i18n
from sugar.bundle.activitybundle import ActivityBundle
from sugar.graphics import style
from sugar import logger
@@ -74,6 +75,10 @@ def main():
parser.add_option('-s', '--single-process', dest='single_process',
action='store_true',
help='start all the instances in the same process')
+ parser.add_option('-i', '--invited', dest='invited',
+ action='store_true',
+ help='the activity is being launched for handling an '
+ 'invite from the network')
(options, args) = parser.parse_args()
logger.start()
@@ -103,9 +108,7 @@ def main():
settings.set_property('gtk-font-name',
'%s %f' % (style.FONT_FACE, style.FONT_SIZE))
- locale_path = None
- if 'SUGAR_LOCALEDIR' in os.environ:
- locale_path = os.environ['SUGAR_LOCALEDIR']
+ locale_path = i18n.get_locale_path(bundle.get_bundle_id())
gettext.bindtextdomain(bundle.get_bundle_id(), locale_path)
gettext.bindtextdomain('sugar-toolkit', sugar.locale_path)
@@ -122,7 +125,8 @@ def main():
activity_constructor = getattr(module, class_name)
activity_handle = activityhandle.ActivityHandle(
activity_id=options.activity_id,
- object_id=options.object_id, uri=options.uri)
+ object_id=options.object_id, uri=options.uri,
+ invited=options.invited)
if options.single_process is True:
sessionbus = dbus.SessionBus()
diff --git a/src/sugar/activity/widgets.py b/src/sugar/activity/widgets.py
index 2867666..b5e4ce7 100644
--- a/src/sugar/activity/widgets.py
+++ b/src/sugar/activity/widgets.py
@@ -108,6 +108,7 @@ class CopyButton(ToolButton):
def __init__(self, **kwargs):
ToolButton.__init__(self, 'edit-copy', **kwargs)
self.props.tooltip = _('Copy')
+ self.props.accelerator = '<Ctrl>C'
class PasteButton(ToolButton):
@@ -115,6 +116,7 @@ class PasteButton(ToolButton):
def __init__(self, **kwargs):
ToolButton.__init__(self, 'edit-paste', **kwargs)
self.props.tooltip = _('Paste')
+ self.props.accelerator = '<Ctrl>V'
class ShareButton(RadioMenuButton):
@@ -147,7 +149,8 @@ class ShareButton(RadioMenuButton):
def __update_share_cb(self, activity):
self.neighborhood.handler_block(self._neighborhood_handle)
try:
- if activity.get_shared():
+ if activity.shared_activity is not None and \
+ not activity.shared_activity.props.private:
self.private.props.sensitive = False
self.neighborhood.props.sensitive = False
self.neighborhood.props.active = True
@@ -202,9 +205,10 @@ class TitleEntry(gtk.ToolItem):
self.entry.set_text(jobject['title'])
def __title_changed_cb(self, entry, activity):
- if not self._update_title_sid:
- self._update_title_sid = gobject.timeout_add_seconds(
- 1, self.__update_title_cb, activity)
+ if self._update_title_sid is not None:
+ gobject.source_remove(self._update_title_sid)
+ self._update_title_sid = gobject.timeout_add_seconds(
+ 1, self.__update_title_cb, activity)
def __update_title_cb(self, activity):
title = self.entry.get_text()
@@ -333,7 +337,7 @@ class ActivityToolbox(Toolbox):
... your code, inserting all other toolbars you need, like EditToolbar
# Add the toolbox to the activity frame:
- self.set_toolbox(toolbox)
+ self.set_toolbar_box(toolbox)
# And make it visible:
toolbox.show()
"""
diff --git a/src/sugar/bundle/activitybundle.py b/src/sugar/bundle/activitybundle.py
index a1f10b9..3bbc250 100644
--- a/src/sugar/bundle/activitybundle.py
+++ b/src/sugar/bundle/activitybundle.py
@@ -23,8 +23,10 @@ UNSTABLE.
from ConfigParser import ConfigParser
import locale
import os
+import shutil
import tempfile
import logging
+import warnings
from sugar import env
from sugar import util
@@ -51,6 +53,7 @@ class ActivityBundle(Bundle):
self.bundle_exec = None
self._name = None
+ self._local_name = None
self._icon = None
self._bundle_id = None
self._mime_types = None
@@ -69,6 +72,9 @@ class ActivityBundle(Bundle):
if linfo_file:
self._parse_linfo(linfo_file)
+ if self._local_name == None:
+ self._local_name = self._name
+
def _get_manifest(self):
if self._manifest is None:
self._manifest = self._read_manifest()
@@ -147,6 +153,8 @@ class ActivityBundle(Bundle):
self._bundle_id = cp.get(section, 'bundle_id')
# FIXME deprecated
elif cp.has_option(section, 'service_name'):
+ warnings.warn('use bundle_id instead of service_name ' \
+ 'in your activity.info', DeprecationWarning)
self._bundle_id = cp.get(section, 'service_name')
else:
raise MalformedBundleException(
@@ -161,6 +169,8 @@ class ActivityBundle(Bundle):
# FIXME class is deprecated
if cp.has_option(section, 'class'):
+ warnings.warn('use exec instead of class ' \
+ 'in your activity.info', DeprecationWarning)
self.activity_class = cp.get(section, 'class')
elif cp.has_option(section, 'exec'):
self.bundle_exec = cp.get(section, 'exec')
@@ -217,7 +227,7 @@ class ActivityBundle(Bundle):
section = 'Activity'
if cp.has_option(section, 'name'):
- self._name = cp.get(section, 'name')
+ self._local_name = cp.get(section, 'name')
if cp.has_option(section, 'tags'):
tag_list = cp.get(section, 'tags').strip(';')
@@ -240,7 +250,11 @@ class ActivityBundle(Bundle):
return self._path
def get_name(self):
- """Get the activity user visible name."""
+ """Get the activity user-visible name."""
+ return self._local_name
+
+ def get_bundle_name(self):
+ """Get the activity bundle name."""
return self._name
def get_installation_time(self):
@@ -380,7 +394,7 @@ class ActivityBundle(Bundle):
os.unlink(dst)
os.symlink(src, dst)
- def uninstall(self, install_path, force=False):
+ def uninstall(self, install_path, force=False, delete_profile=False):
if os.path.islink(install_path):
# Don't remove the actual activity dir if it's a symbolic link
# because we may be removing user data.
@@ -409,6 +423,12 @@ class ActivityBundle(Bundle):
os.readlink(path).startswith(install_path):
os.remove(path)
+ if delete_profile:
+ bundle_profile_path = env.get_profile_path(self._bundle_id)
+ if os.path.exists(bundle_profile_path):
+ os.chmod(bundle_profile_path, 0775)
+ shutil.rmtree(bundle_profile_path, ignore_errors=True)
+
self._uninstall(install_path)
def is_user_activity(self):
diff --git a/src/sugar/bundle/bundle.py b/src/sugar/bundle/bundle.py
index c9763a0..cb110cc 100644
--- a/src/sugar/bundle/bundle.py
+++ b/src/sugar/bundle/bundle.py
@@ -68,10 +68,9 @@ class Bundle(object):
def __init__(self, path):
self._path = path
self._zip_root_dir = None
+ self._zip_file = None
- if os.path.isdir(self._path):
- self._zip_file = None
- else:
+ if not os.path.isdir(self._path):
self._zip_file = zipfile.ZipFile(self._path)
self._check_zip_bundle()
diff --git a/src/sugar/bundle/contentbundle.py b/src/sugar/bundle/contentbundle.py
index 4b483cb..48e05a1 100644
--- a/src/sugar/bundle/contentbundle.py
+++ b/src/sugar/bundle/contentbundle.py
@@ -74,18 +74,6 @@ class ContentBundle(Bundle):
section = 'Library'
- if cp.has_option(section, 'host_version'):
- version = cp.get(section, 'host_version')
- try:
- if int(version) != 1:
- raise MalformedBundleException(
- 'Content bundle %s has unknown host_version '
- 'number %s' % (self._path, version))
- except ValueError:
- raise MalformedBundleException(
- 'Content bundle %s has invalid host_version number %s' %
- (self._path, version))
-
if cp.has_option(section, 'name'):
self._name = cp.get(section, 'name')
else:
diff --git a/src/sugar/datastore/Makefile.am b/src/sugar/datastore/Makefile.am
index a5f16b7..81d760c 100644
--- a/src/sugar/datastore/Makefile.am
+++ b/src/sugar/datastore/Makefile.am
@@ -1,5 +1,4 @@
sugardir = $(pythondir)/sugar/datastore
sugar_PYTHON = \
__init__.py \
- dbus_helpers.py \
datastore.py
diff --git a/src/sugar/datastore/datastore.py b/src/sugar/datastore/datastore.py
index 8d23721..3f5188e 100644
--- a/src/sugar/datastore/datastore.py
+++ b/src/sugar/datastore/datastore.py
@@ -1,4 +1,5 @@
# Copyright (C) 2007, One Laptop Per Child
+# Copyright (C) 2010, Simon Schampijer
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -16,7 +17,7 @@
# Boston, MA 02111-1307, USA.
"""
-STABLE.
+STABLE
"""
import logging
@@ -27,75 +28,150 @@ import tempfile
import gobject
import gconf
import gio
+import dbus
+import dbus.glib
from sugar import env
-from sugar.datastore import dbus_helpers
from sugar import mime
+from sugar import dispatch
+
+DS_DBUS_SERVICE = "org.laptop.sugar.DataStore"
+DS_DBUS_INTERFACE = "org.laptop.sugar.DataStore"
+DS_DBUS_PATH = "/org/laptop/sugar/DataStore"
+
+_data_store = None
+
+
+def _get_data_store():
+ global _data_store
+
+ if not _data_store:
+ _bus = dbus.SessionBus()
+ _data_store = dbus.Interface(_bus.get_object(DS_DBUS_SERVICE,
+ DS_DBUS_PATH),
+ DS_DBUS_INTERFACE)
+ _data_store.connect_to_signal('Created', __datastore_created_cb)
+ _data_store.connect_to_signal('Deleted', __datastore_deleted_cb)
+ _data_store.connect_to_signal('Updated', __datastore_updated_cb)
+
+ return _data_store
+
+
+def __datastore_created_cb(object_id):
+ metadata = _get_data_store().get_properties(object_id, byte_arrays=True)
+ updated.send(None, object_id=object_id, metadata=metadata)
+
+
+def __datastore_updated_cb(object_id):
+ metadata = _get_data_store().get_properties(object_id, byte_arrays=True)
+ updated.send(None, object_id=object_id, metadata=metadata)
+
+
+def __datastore_deleted_cb(object_id):
+ deleted.send(None, object_id=object_id)
+
+created = dispatch.Signal()
+deleted = dispatch.Signal()
+updated = dispatch.Signal()
+
+_get_data_store()
class DSMetadata(gobject.GObject):
+ """A representation of the metadata associated with a DS entry."""
__gsignals__ = {
'updated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
}
- def __init__(self, props=None):
+ def __init__(self, properties=None):
gobject.GObject.__init__(self)
- if not props:
- self._props = {}
+ if not properties:
+ self._properties = {}
else:
- self._props = props
+ self._properties = properties
default_keys = ['activity', 'activity_id',
'mime_type', 'title_set_by_user']
for key in default_keys:
- if not self._props.has_key(key):
- self._props[key] = ''
+ if key not in self._properties:
+ self._properties[key] = ''
def __getitem__(self, key):
- return self._props[key]
+ return self._properties[key]
def __setitem__(self, key, value):
- if not self._props.has_key(key) or self._props[key] != value:
- self._props[key] = value
+ if key not in self._properties or self._properties[key] != value:
+ self._properties[key] = value
self.emit('updated')
def __delitem__(self, key):
- del self._props[key]
+ del self._properties[key]
def __contains__(self, key):
- return self._props.__contains__(key)
+ return self._properties.__contains__(key)
def has_key(self, key):
- return self._props.has_key(key)
+ logging.warning(".has_key() is deprecated, use 'in'")
+ return key in self._properties
def keys(self):
- return self._props.keys()
+ return self._properties.keys()
def get_dictionary(self):
- return self._props
+ return self._properties
def copy(self):
- return DSMetadata(self._props.copy())
+ return DSMetadata(self._properties.copy())
def get(self, key, default=None):
- if self._props.has_key(key):
- return self._props[key]
+ if key in self._properties:
+ return self._properties[key]
else:
return default
+ def update(self, properties):
+ """Update all of the metadata"""
+ for (key, value) in properties.items():
+ self[key] = value
+
class DSObject(object):
+ """A representation of a DS entry."""
def __init__(self, object_id, metadata=None, file_path=None):
- self.object_id = object_id
+ self._update_signal_match = None
+ self._object_id = None
+
+ self.set_object_id(object_id)
+
self._metadata = metadata
self._file_path = file_path
self._destroyed = False
self._owns_file = False
+ def get_object_id(self):
+ return self._object_id
+
+ def set_object_id(self, object_id):
+ if self._update_signal_match is not None:
+ self._update_signal_match.remove()
+ if object_id is not None:
+ self._update_signal_match = _get_data_store().connect_to_signal(
+ 'Updated', self.__object_updated_cb, arg0=object_id)
+
+ self._object_id = object_id
+
+ object_id = property(get_object_id, set_object_id)
+
+ def __object_updated_cb(self, object_id):
+ properties = _get_data_store().get_properties(self._object_id,
+ byte_arrays=True)
+ self._metadata.update(properties)
+
def get_metadata(self):
if self._metadata is None and not self.object_id is None:
- metadata = DSMetadata(dbus_helpers.get_properties(self.object_id))
+ properties = _get_data_store().get_properties(self.object_id)
+ metadata = DSMetadata(properties)
self._metadata = metadata
return self._metadata
@@ -107,7 +183,7 @@ class DSObject(object):
def get_file_path(self, fetch=True):
if fetch and self._file_path is None and not self.object_id is None:
- self.set_file_path(dbus_helpers.get_filename(self.object_id))
+ self.set_file_path(_get_data_store().get_filename(self.object_id))
self._owns_file = True
return self._file_path
@@ -141,7 +217,12 @@ class DSObject(object):
def copy(self):
return DSObject(None, self._metadata.copy(), self._file_path)
+
class RawObject(object):
+ """A representation for objects not in the DS but
+ in the file system.
+
+ """
def __init__(self, file_path):
stat = os.stat(file_path)
@@ -172,9 +253,11 @@ class RawObject(object):
# to create hardlinks to jobject files
# and w/o this, it wouldn't work since we have file from mounted device
if self._file_path is None:
+ data_path = os.path.join(env.get_profile_path(), 'data')
self._file_path = tempfile.mktemp(
- prefix='rawobject',
- dir=os.path.join(env.get_profile_path(), 'data'))
+ prefix='rawobject', dir=data_path)
+ if not os.path.exists(data_path):
+ os.makedirs(data_path)
os.symlink(self.object_id, self._file_path)
return self._file_path
@@ -198,12 +281,20 @@ class RawObject(object):
def get(object_id):
+ """Get the properties of the object with the ID given.
+
+ Keyword arguments:
+ object_id -- unique identifier of the object
+
+ Return: a DSObject
+
+ """
logging.debug('datastore.get')
if object_id.startswith('/'):
return RawObject(object_id)
- metadata = dbus_helpers.get_properties(object_id)
+ metadata = _get_data_store().get_properties(object_id, byte_arrays=True)
ds_object = DSObject(object_id, DSMetadata(metadata), None)
# TODO: register the object for updates
@@ -211,14 +302,59 @@ def get(object_id):
def create():
+ """Create a new DSObject.
+
+ Return: a DSObject
+
+ """
metadata = DSMetadata()
metadata['mtime'] = datetime.now().isoformat()
metadata['timestamp'] = int(time.time())
return DSObject(object_id=None, metadata=metadata, file_path=None)
+def _update_ds_entry(uid, properties, filename, transfer_ownership=False,
+ reply_handler=None, error_handler=None, timeout=-1):
+ debug_properties = properties.copy()
+ if "preview" in debug_properties:
+ debug_properties["preview"] = "<omitted>"
+ logging.debug('dbus_helpers.update: %s, %s, %s, %s', uid, filename,
+ debug_properties, transfer_ownership)
+ if reply_handler and error_handler:
+ _get_data_store().update(uid, dbus.Dictionary(properties), filename,
+ transfer_ownership,
+ reply_handler=reply_handler,
+ error_handler=error_handler,
+ timeout=timeout)
+ else:
+ _get_data_store().update(uid, dbus.Dictionary(properties),
+ filename, transfer_ownership)
+
+
+def _create_ds_entry(properties, filename, transfer_ownership=False):
+ object_id = _get_data_store().create(dbus.Dictionary(properties), filename,
+ transfer_ownership)
+ return object_id
+
+
def write(ds_object, update_mtime=True, transfer_ownership=False,
reply_handler=None, error_handler=None, timeout=-1):
+ """Write the DSObject given to the datastore. Creates a new entry if
+ the entry does not exist yet.
+
+ Keyword arguments:
+ update_mtime -- boolean if the mtime of the entry should be regenerated
+ (default True)
+ transfer_ownership -- set it to true if the ownership of the entry should
+ be passed - who is responsible to delete the file
+ when done with it (default False)
+ reply_handler -- will be called with the method's return values as
+ arguments (default None)
+ error_handler -- will be called with an instance of a DBusException
+ representing a remote exception (default None)
+ timeout -- dbus timeout for the caller to wait (default -1)
+
+ """
logging.debug('datastore.write')
properties = ds_object.metadata.get_dictionary().copy()
@@ -234,33 +370,71 @@ def write(ds_object, update_mtime=True, transfer_ownership=False,
# FIXME: this func will be sync for creates regardless of the handlers
# supplied. This is very bad API, need to decide what to do here.
if ds_object.object_id:
- dbus_helpers.update(ds_object.object_id,
- properties,
- file_path,
- transfer_ownership,
- reply_handler=reply_handler,
- error_handler=error_handler,
- timeout=timeout)
+ _update_ds_entry(ds_object.object_id,
+ properties,
+ file_path,
+ transfer_ownership,
+ reply_handler=reply_handler,
+ error_handler=error_handler,
+ timeout=timeout)
else:
if reply_handler or error_handler:
logging.warning('datastore.write() cannot currently be called' \
'async for creates, see ticket 3071')
- ds_object.object_id = dbus_helpers.create(properties,
- file_path,
- transfer_ownership)
+ ds_object.object_id = _create_ds_entry(properties, file_path,
+ transfer_ownership)
ds_object.metadata['uid'] = ds_object.object_id
# TODO: register the object for updates
logging.debug('Written object %s to the datastore.', ds_object.object_id)
def delete(object_id):
+ """Delete the datastore entry with the given uid.
+
+ Keyword arguments:
+ object_id -- uid of the datastore entry
+
+ """
logging.debug('datastore.delete')
- dbus_helpers.delete(object_id)
+ _get_data_store().delete(object_id)
def find(query, sorting=None, limit=None, offset=None, properties=None,
reply_handler=None, error_handler=None):
-
+ """Find DS entries that match the query provided.
+
+ Keyword arguments:
+ query -- a dictionary containing metadata key value pairs
+ for a fulltext search use the key 'query' e.g. {'query': 'blue*'}
+ other possible well-known properties are:
+ 'activity': 'my.organization.MyActivity'
+ 'activity_id': '6f7f3acacca87886332f50bdd522d805f0abbf1f'
+ 'title': 'My new project'
+ 'title_set_by_user': '0'
+ 'keep': '0'
+ 'ctime': '1972-05-12T18:41:08'
+ 'mtime': '2007-06-16T03:42:33'
+ 'timestamp': 1192715145
+ 'preview': ByteArray(png file data, 300x225 px)
+ 'icon-color': '#ff0000,#ffff00'
+ 'mime_type': 'application/x-my-activity'
+ 'share-scope': # if shared
+ 'buddies': '{}'
+ 'description': 'some longer text'
+ 'tags': 'one two'
+ sorting -- key to order results by e.g. 'timestamp' (default None)
+ limit -- return only limit results (default None)
+ offset -- return only results starting at offset (default None)
+ properties -- you can specify here a list of metadata you want to be
+ present in the result e.g. ['title, 'keep'] (default None)
+ reply_handler -- will be called with the method's return values as
+ arguments (default None)
+ error_handler -- will be called with an instance of a DBusException
+ representing a remote exception (default None)
+
+ Return: DSObjects matching the query, number of matches
+
+ """
query = query.copy()
if properties is None:
@@ -273,57 +447,103 @@ def find(query, sorting=None, limit=None, offset=None, properties=None,
if offset:
query['offset'] = offset
- props_list, total_count = dbus_helpers.find(query, properties,
- reply_handler, error_handler)
+ if reply_handler and error_handler:
+ _get_data_store().find(query, properties,
+ reply_handler=reply_handler,
+ error_handler=error_handler,
+ byte_arrays=True)
+ return
+ else:
+ entries, total_count = _get_data_store().find(query, properties,
+ byte_arrays=True)
+ ds_objects = []
+ for entry in entries:
+ object_id = entry['uid']
+ del entry['uid']
- objects = []
- for props in props_list:
- object_id = props['uid']
- del props['uid']
+ ds_object = DSObject(object_id, DSMetadata(entry), None)
+ ds_objects.append(ds_object)
- ds_object = DSObject(object_id, DSMetadata(props), None)
- objects.append(ds_object)
+ return ds_objects, total_count
- return objects, total_count
+def copy(ds_object, mount_point):
+ """Copy a datastore entry
-def copy(jobject, mount_point):
+ Keyword arguments:
+ ds_object -- DSObject to copy
+ mount_point -- mount point of the new datastore entry
- new_jobject = jobject.copy()
- new_jobject.metadata['mountpoint'] = mount_point
+ """
+ new_ds_object = ds_object.copy()
+ new_ds_object.metadata['mountpoint'] = mount_point
- if jobject.metadata.has_key('title'):
- filename = jobject.metadata['title']
+ if 'title' in ds_object.metadata:
+ filename = ds_object.metadata['title']
- if jobject.metadata.has_key('mime_type'):
- mime_type = jobject.metadata['mime_type']
+ if 'mime_type' in ds_object.metadata:
+ mime_type = ds_object.metadata['mime_type']
extension = mime.get_primary_extension(mime_type)
if extension:
filename += '.' + extension
- new_jobject.metadata['suggested_filename'] = filename
+ new_ds_object.metadata['suggested_filename'] = filename
# this will cause the file be retrieved from the DS
- new_jobject.file_path = jobject.file_path
+ new_ds_object.file_path = ds_object.file_path
- write(new_jobject)
+ write(new_ds_object)
def mount(uri, options, timeout=-1):
- return dbus_helpers.mount(uri, options, timeout=timeout)
+ """Deprecated. API private to the shell. Mount a device.
+
+ Keyword arguments:
+ uri -- identifier of the device
+ options -- mount options
+ timeout -- dbus timeout for the caller to wait (default -1)
+
+ Return: empty string
+
+ """
+ return _get_data_store().mount(uri, options, timeout=timeout)
def unmount(mount_point_id):
- dbus_helpers.unmount(mount_point_id)
+ """Deprecated. API private to the shell.
+
+ Keyword arguments:
+ mount_point_id -- id of the mount point
+
+ Note: API private to the shell.
+
+ """
+ _get_data_store().unmount(mount_point_id)
def mounts():
- return dbus_helpers.mounts()
+ """Deprecated. Returns the mount point of the datastore. We get mount
+ points through gio now. API private to the shell.
+
+ Return: datastore mount point
+
+ """
+ return _get_data_store().mounts()
def complete_indexing():
- return dbus_helpers.complete_indexing()
+ """Deprecated. API private to the shell."""
+ logging.warning('The method complete_indexing has been deprecated.')
def get_unique_values(key):
- return dbus_helpers.get_unique_values(key)
+ """Retrieve an array of unique values for a field.
+
+ Keyword arguments:
+ key -- only the property activity is currently supported
+
+ Return: list of activities
+
+ """
+ return _get_data_store().get_uniquevaluesfor(
+ key, dbus.Dictionary({}, signature='ss'))
diff --git a/src/sugar/datastore/dbus_helpers.py b/src/sugar/datastore/dbus_helpers.py
deleted file mode 100644
index 008b7a0..0000000
--- a/src/sugar/datastore/dbus_helpers.py
+++ /dev/null
@@ -1,115 +0,0 @@
-# Copyright (C) 2006-2007 Red Hat, Inc.
-# Copyright (C) 2007, One Laptop Per Child
-#
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2 of the License, or (at your option) any later version.
-#
-# This library 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
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library; if not, write to the
-# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
-# Boston, MA 02111-1307, USA.
-
-"""
-UNSTABLE. Should be internal to the datastore module.
-"""
-
-import logging
-
-import dbus
-import dbus.glib
-
-DS_DBUS_SERVICE = "org.laptop.sugar.DataStore"
-DS_DBUS_INTERFACE = "org.laptop.sugar.DataStore"
-DS_DBUS_PATH = "/org/laptop/sugar/DataStore"
-
-_data_store = None
-
-
-def _get_data_store():
- global _data_store
-
- if not _data_store:
- _bus = dbus.SessionBus()
- _data_store = dbus.Interface(_bus.get_object(DS_DBUS_SERVICE,
- DS_DBUS_PATH),
- DS_DBUS_INTERFACE)
- return _data_store
-
-
-def create(properties, filename, transfer_ownership=False):
- object_id = _get_data_store().create(dbus.Dictionary(properties), filename,
- transfer_ownership)
- logging.debug('dbus_helpers.create: ' + object_id)
- return object_id
-
-
-def update(uid, properties, filename, transfer_ownership=False,
- reply_handler=None, error_handler=None, timeout=-1):
- debug_props = properties.copy()
- if debug_props.has_key("preview"):
- debug_props["preview"] = "<omitted>"
- logging.debug('dbus_helpers.update: %s, %s, %s, %s', uid, filename,
- debug_props, transfer_ownership)
- if reply_handler and error_handler:
- _get_data_store().update(uid, dbus.Dictionary(properties), filename,
- transfer_ownership,
- reply_handler=reply_handler,
- error_handler=error_handler,
- timeout=timeout)
- else:
- _get_data_store().update(uid, dbus.Dictionary(properties),
- filename, transfer_ownership)
-
-
-def delete(uid):
- logging.debug('dbus_helpers.delete: %r', uid)
- _get_data_store().delete(uid)
-
-
-def get_properties(uid):
- logging.debug('dbus_helpers.get_properties: %s', uid)
- return _get_data_store().get_properties(uid, byte_arrays=True)
-
-
-def get_filename(uid):
- filename = _get_data_store().get_filename(uid)
- logging.debug('dbus_helpers.get_filename: %s, %s', uid, filename)
- return filename
-
-
-def find(query, properties, reply_handler, error_handler):
- logging.debug('dbus_helpers.find: %r %r', query, properties)
- if reply_handler and error_handler:
- return _get_data_store().find(query, properties,
- reply_handler=reply_handler, error_handler=error_handler,
- byte_arrays=True)
- else:
- return _get_data_store().find(query, properties, byte_arrays=True)
-
-
-def mount(uri, options, timeout=-1):
- return _get_data_store().mount(uri, options, timeout=timeout)
-
-
-def unmount(mount_point_id):
- _get_data_store().unmount(mount_point_id)
-
-
-def mounts():
- return _get_data_store().mounts()
-
-
-def get_unique_values(key):
- return _get_data_store().get_uniquevaluesfor(
- key, dbus.Dictionary({}, signature='ss'))
-
-
-def complete_indexing():
- return _get_data_store().complete_indexing()
diff --git a/src/sugar/graphics/alert.py b/src/sugar/graphics/alert.py
index 4441909..a4dd017 100644
--- a/src/sugar/graphics/alert.py
+++ b/src/sugar/graphics/alert.py
@@ -28,6 +28,7 @@ create a simple alert message.
STABLE.
"""
# Copyright (C) 2007, One Laptop Per Child
+# Copyright (C) 2010, Anish Mangal <anishmangal2002@gmail.com>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -290,6 +291,50 @@ class ConfirmationAlert(Alert):
self.add_button(gtk.RESPONSE_OK, _('Ok'), icon)
icon.show()
+class ErrorAlert(Alert):
+ """
+ This is a ready-made one button (Ok) alert.
+
+ An error alert is a nice shortcut from a standard Alert because it
+ comes with the 'OK' button already built-in. When clicked, the
+ 'OK' button will emit a response with a response_id of gtk.RESPONSE_OK.
+
+ Examples
+ --------
+
+ .. code-block:: python
+ from sugar.graphics.alert import ErrorAlert
+ ...
+ #### Method: _alert_error, create a Error alert (with ok
+ button standard)
+ # and add it to the UI.
+ def _alert_error(self):
+ alert = ErrorAlert()
+ alert.props.title=_('Title of Alert Goes Here')
+ alert.props.msg = _('Text message of alert goes here')
+ alert.connect('response', self._alert_response_cb)
+ self.add_alert(alert)
+
+
+ #### Method: _alert_response_cb, called when an alert object throws a
+ response event.
+ def _alert_response_cb(self, alert, response_id):
+ #remove the alert from the screen, since either a response button
+ #was clicked or there was a timeout
+ self.remove_alert(alert)
+
+ #Do any work that is specific to the response_id.
+ if response_id is gtk.RESPONSE_OK:
+ print 'Ok Button was clicked. Do any work upon ok here ...'
+
+ """
+
+ def __init__(self, **kwargs):
+ Alert.__init__(self, **kwargs)
+
+ icon = Icon(icon_name='dialog-ok')
+ self.add_button(gtk.RESPONSE_OK, _('Ok'), icon)
+ icon.show()
class _TimeoutIcon(hippo.CanvasText, hippo.CanvasItem):
"""An icon with a round border"""
diff --git a/src/sugar/graphics/icon.py b/src/sugar/graphics/icon.py
index 94c66aa..4a94479 100644
--- a/src/sugar/graphics/icon.py
+++ b/src/sugar/graphics/icon.py
@@ -1146,3 +1146,31 @@ def get_icon_file_name(icon_name):
filename = info.get_filename()
del info
return filename
+
+
+def get_surface(**kwargs):
+ """Get cached cairo surface.
+
+ Keyword arguments:
+ icon_name -- name of icon to load, default None
+ file_name -- path to image file, default None
+ fill_color -- for svg images, change default fill color
+ default None
+ stroke_color -- for svg images, change default stroke color
+ default None
+ background_color -- draw background or surface will be transparent
+ default None
+ badge_name -- name of icon which will be drawn on top of
+ original image, default None
+ width -- change image width, default None
+ height -- change image height, default None
+ cache -- if image is svg, keep svg file content for later
+ scale -- scale image, default 1.0
+
+ Return: cairo surface or None if image was not found
+
+ """
+ icon = _IconBuffer()
+ for key, value in kwargs.items():
+ icon.__setattr__(key, value)
+ return icon.get_surface()
diff --git a/src/sugar/graphics/palette.py b/src/sugar/graphics/palette.py
index 6ce92f6..d4632eb 100644
--- a/src/sugar/graphics/palette.py
+++ b/src/sugar/graphics/palette.py
@@ -164,7 +164,6 @@ class Palette(PaletteWindow):
self.menu.set_active(True)
def __hide_cb(self, widget):
- logging.debug('__hide_cb')
self.menu.set_active(False)
self.menu.cancel()
self._secondary_anim.stop()
@@ -183,8 +182,6 @@ class Palette(PaletteWindow):
return self._full_request
def popup(self, immediate=False, state=None):
- logging.debug('Palette.popup immediate %r', immediate)
-
if self._invoker is not None:
self._update_full_request()
diff --git a/src/sugar/graphics/palettewindow.py b/src/sugar/graphics/palettewindow.py
index 3049f55..22131c2 100644
--- a/src/sugar/graphics/palettewindow.py
+++ b/src/sugar/graphics/palettewindow.py
@@ -326,8 +326,6 @@ class PaletteWindow(gtk.Window):
self.update_position()
def popdown(self, immediate=False):
- logging.debug('PaletteWindow.popdown immediate %r', immediate)
-
self._popup_anim.stop()
self._mouse_detector.stop()
@@ -379,8 +377,6 @@ class PaletteWindow(gtk.Window):
self.emit('popup')
def __hide_cb(self, widget):
- logging.debug('__hide_cb')
-
if self._invoker:
self._invoker.notify_popdown()
diff --git a/src/sugar/graphics/style.py b/src/sugar/graphics/style.py
index 929b78e..2828b7f 100644
--- a/src/sugar/graphics/style.py
+++ b/src/sugar/graphics/style.py
@@ -141,6 +141,7 @@ COLOR_BUTTON_GREY = Color('#808080')
COLOR_INACTIVE_FILL = Color('#9D9FA1')
COLOR_INACTIVE_STROKE = Color('#757575')
COLOR_TEXT_FIELD_GREY = Color('#E5E5E5')
+COLOR_HIGHLIGHT = Color('#E7E7E7')
PALETTE_CURSOR_DISTANCE = zoom(10)
diff --git a/src/sugar/presence/Makefile.am b/src/sugar/presence/Makefile.am
index 0c4368b..6d87fe7 100644
--- a/src/sugar/presence/Makefile.am
+++ b/src/sugar/presence/Makefile.am
@@ -3,6 +3,7 @@ sugar_PYTHON = \
__init__.py \
activity.py \
buddy.py \
+ connectionmanager.py \
sugartubeconn.py \
tubeconn.py \
presenceservice.py
diff --git a/src/sugar/presence/activity.py b/src/sugar/presence/activity.py
index 1d4a9c9..aa6b396 100644
--- a/src/sugar/presence/activity.py
+++ b/src/sugar/presence/activity.py
@@ -1,4 +1,5 @@
# Copyright (C) 2007, Red Hat, Inc.
+# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -21,11 +22,27 @@ STABLE.
"""
import logging
+from functools import partial
import dbus
+from dbus import PROPERTIES_IFACE
import gobject
-import telepathy
-
+from telepathy.client import Channel
+from telepathy.interfaces import CHANNEL, \
+ CHANNEL_INTERFACE_GROUP, \
+ CHANNEL_TYPE_TUBES, \
+ CHANNEL_TYPE_TEXT, \
+ CONNECTION, \
+ PROPERTIES_INTERFACE
+from telepathy.constants import CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES, \
+ HANDLE_TYPE_ROOM, \
+ HANDLE_TYPE_CONTACT, \
+ PROPERTY_FLAG_WRITE
+
+from sugar.presence.buddy import Buddy
+
+CONN_INTERFACE_ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties'
+CONN_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo'
_logger = logging.getLogger('sugar.presence.activity')
@@ -64,66 +81,72 @@ class Activity(gobject.GObject):
'joined': (bool, None, None, False, gobject.PARAM_READABLE),
}
- _PRESENCE_SERVICE = "org.laptop.Sugar.Presence"
- _ACTIVITY_DBUS_INTERFACE = "org.laptop.Sugar.Presence.Activity"
+ def __init__(self, account_path, connection, room_handle=None,
+ properties=None):
+ if room_handle is None and properties is None:
+ raise ValueError('Need to pass one of room_handle or properties')
+
+ if properties is None:
+ properties = {}
- def __init__(self, bus, new_obj_cb, del_obj_cb, object_path):
- """Initialse the activity interface, connecting to service"""
gobject.GObject.__init__(self)
- self.telepathy_room_handle = None
- self._object_path = object_path
- self._ps_new_object = new_obj_cb
- self._ps_del_object = del_obj_cb
- bobj = bus.get_object(self._PRESENCE_SERVICE, object_path)
- self._activity = dbus.Interface(bobj, self._ACTIVITY_DBUS_INTERFACE)
- self._activity.connect_to_signal('BuddyHandleJoined',
- self._buddy_handle_joined_cb)
- self._activity.connect_to_signal('BuddyLeft',
- self._buddy_left_cb)
- self._activity.connect_to_signal('NewChannel', self._new_channel_cb)
- self._activity.connect_to_signal('PropertiesChanged',
- self._properties_changed_cb,
- utf8_strings=True)
- # FIXME: this *would* just use a normal proxy call, but I want the
- # pending call object so I can block on it, and normal proxy methods
- # don't return those as of dbus-python 0.82.1; so do it the hard way
- self._get_properties_call = bus.call_async(self._PRESENCE_SERVICE,
- object_path, self._ACTIVITY_DBUS_INTERFACE, 'GetProperties',
- '', (), self._get_properties_reply_cb,
- self._get_properties_error_cb, utf8_strings=True)
-
- self._id = None
- self._color = None
- self._name = None
- self._type = None
- self._tags = None
- self._private = True
- self._joined = False
- # Cache for get_buddy_by_handle, maps handles to buddy object paths
- self._handle_to_buddy_path = {}
- self._buddy_path_to_handle = {}
- # Set up by set_up_tubes()
- self.telepathy_conn = None
- self.telepathy_tubes_chan = None
+ self._account_path = account_path
+ self.telepathy_conn = connection
self.telepathy_text_chan = None
- self._telepathy_room = None
+ self.telepathy_tubes_chan = None
- def __repr__(self):
- return ('<proxy for %s at %x>' % (self._object_path, id(self)))
+ self.room_handle = room_handle
+ self._join_command = None
+ self._share_command = None
+ self._id = properties.get('id', None)
+ self._color = properties.get('color', None)
+ self._name = properties.get('name', None)
+ self._type = properties.get('type', None)
+ self._tags = properties.get('tags', None)
+ self._private = properties.get('private', True)
+ self._joined = properties.get('joined', False)
+ self._channel_self_handle = None
+ self._text_channel_group_flags = 0
+ self._buddies = {}
- def _get_properties_reply_cb(self, new_props):
self._get_properties_call = None
- _logger.debug('%r: initial GetProperties returned', self)
- self._properties_changed_cb(new_props)
-
- def _get_properties_error_cb(self, e):
+ if not self.room_handle is None:
+ self._start_tracking_properties()
+
+ def _start_tracking_properties(self):
+ bus = dbus.SessionBus()
+ self._get_properties_call = bus.call_async(
+ self.telepathy_conn.requested_bus_name,
+ self.telepathy_conn.object_path,
+ CONN_INTERFACE_ACTIVITY_PROPERTIES,
+ 'GetProperties',
+ 'u',
+ (self.room_handle,),
+ reply_handler=self.__got_properties_cb,
+ error_handler=self.__error_handler_cb,
+ utf8_strings=True)
+
+ # As only one Activity instance is needed per activity process,
+ # we can afford listening to ActivityPropertiesChanged like this.
+ self.telepathy_conn.connect_to_signal(
+ 'ActivityPropertiesChanged',
+ self.__activity_properties_changed_cb,
+ dbus_interface=CONN_INTERFACE_ACTIVITY_PROPERTIES)
+
+ def __activity_properties_changed_cb(self, room_handle, properties):
+ _logger.debug('%r: Activity properties changed to %r', self, properties)
+ self._update_properties(properties)
+
+ def __got_properties_cb(self, properties):
+ _logger.debug('__got_properties_cb %r', properties)
self._get_properties_call = None
- # FIXME: do something with the error
- _logger.warning('%r: Error doing initial GetProperties: %s', self, e)
+ self._update_properties(properties)
+
+ def __error_handler_cb(self, error):
+ _logger.debug('__error_handler_cb %r', error)
- def _properties_changed_cb(self, new_props):
- _logger.debug('%r: Activity properties changed to %r', self, new_props)
+ def _update_properties(self, new_props):
val = new_props.get('name', self._name)
if isinstance(val, str) and val != self._name:
self._name = val
@@ -181,79 +204,33 @@ class Activity(gobject.GObject):
"""Set a particular property in our property dictionary"""
# FIXME: need an asynchronous API to set these properties,
# particularly 'private'
+
if pspec.name == "name":
- self._activity.SetProperties({'name': val})
self._name = val
elif pspec.name == "color":
- self._activity.SetProperties({'color': val})
self._color = val
elif pspec.name == "tags":
- self._activity.SetProperties({'tags': val})
self._tags = val
elif pspec.name == "private":
- self._activity.SetProperties({'private': val})
self._private = val
+ else:
+ raise ValueError('Unknown property "%s"', pspec.name)
+
+ self._publish_properties()
def set_private(self, val, reply_handler, error_handler):
+ _logger.debug('set_private %r', val)
self._activity.SetProperties({'private': bool(val)},
reply_handler=reply_handler,
error_handler=error_handler)
- def _emit_buddy_joined_signal(self, object_path):
- """Generate buddy-joined GObject signal with presence Buddy object"""
- self.emit('buddy-joined', self._ps_new_object(object_path))
- return False
-
- def _buddy_handle_joined_cb(self, object_path, handle):
- _logger.debug('%r: buddy %s joined with handle %u', self, object_path,
- handle)
- gobject.idle_add(self._emit_buddy_joined_signal, object_path)
- self._handle_to_buddy_path[handle] = object_path
- self._buddy_path_to_handle[object_path] = handle
-
- def _emit_buddy_left_signal(self, object_path):
- """Generate buddy-left GObject signal with presence Buddy object
-
- XXX note use of _ps_new_object instead of _ps_del_object here
- """
- self.emit('buddy-left', self._ps_new_object(object_path))
- return False
-
- def _buddy_left_cb(self, object_path):
- _logger.debug('%r: buddy %s left', self, object_path)
- gobject.idle_add(self._emit_buddy_left_signal, object_path)
- handle = self._buddy_path_to_handle.pop(object_path, None)
- if handle:
- self._handle_to_buddy_path.pop(handle, None)
-
- def _emit_new_channel_signal(self, object_path):
- """Generate new-channel GObject signal with channel object path
-
- New telepathy-python communications channel has been opened
- """
- self.emit('new-channel', object_path)
- return False
-
- def _new_channel_cb(self, object_path):
- _logger.debug('%r: new channel created at %s', self, object_path)
- gobject.idle_add(self._emit_new_channel_signal, object_path)
-
def get_joined_buddies(self):
"""Retrieve the set of Buddy objects attached to this activity
returns list of presence Buddy objects that we can successfully
create from the buddy object paths that PS has for this activity.
"""
- resp = self._activity.GetJoinedBuddies()
- buddies = []
- for item in resp:
- try:
- buddies.append(self._ps_new_object(item))
- except dbus.DBusException:
- _logger.debug(
- 'get_joined_buddies failed to get buddy object for %r',
- item)
- return buddies
+ return self._buddies.values()
def get_buddy_by_handle(self, handle):
"""Retrieve the Buddy object given a telepathy handle.
@@ -273,109 +250,186 @@ class Activity(gobject.GObject):
The callback will be called with one parameter: None on success,
or an exception on failure.
"""
- op = buddy.object_path()
- _logger.debug('%r: inviting %s', self, op)
- self._activity.Invite(op, message,
- reply_handler=lambda: response_cb(None),
- error_handler=response_cb)
+ if not self._joined:
+ raise RuntimeError('Cannot invite a buddy to an activity that is'
+ 'not shared.')
+ self.telepathy_text_chan.AddMembers([buddy.contact_handle], message,
+ dbus_interface=CHANNEL_INTERFACE_GROUP,
+ reply_handler=partial(self.__invite_cb, response_cb),
+ error_handler=partial(self.__invite_cb, response_cb))
- # Joining and sharing (FIXME: sharing is actually done elsewhere)
+ def __invite_cb(self, response_cb, error=None):
+ response_cb(error)
def set_up_tubes(self, reply_handler, error_handler):
+ raise NotImplementedError()
+
+ def __joined_cb(self, join_command, error):
+ _logger.debug('%r: Join finished %r', self, error)
+ if error is not None:
+ self.emit('joined', error is None, str(error))
+ self.telepathy_text_chan = join_command.text_channel
+ self.telepathy_tubes_chan = join_command.tubes_channel
+ self._channel_self_handle = join_command.channel_self_handle
+ self._text_channel_group_flags = join_command.text_channel_group_flags
+ self._start_tracking_buddies()
+ self._start_tracking_channel()
+
+ def _start_tracking_buddies(self):
+ group = self.telepathy_text_chan[CHANNEL_INTERFACE_GROUP]
+
+ group.GetAllMembers(reply_handler=self.__get_all_members_cb,
+ error_handler=self.__error_handler_cb)
+
+ group.connect_to_signal('MembersChanged',
+ self.__text_channel_members_changed_cb)
+
+ def _start_tracking_channel(self):
+ channel = self.telepathy_text_chan[CHANNEL]
+ channel.connect_to_signal('Closed', self.__text_channel_closed_cb)
+
+ def __get_all_members_cb(self, members, local_pending, remote_pending):
+ _logger.debug('__get_all_members_cb %r %r', members,
+ self._text_channel_group_flags)
+ if self._channel_self_handle in members:
+ members.remove(self._channel_self_handle)
+
+ if not members:
+ return
- chans = []
-
- def tubes_ready():
- if self.telepathy_text_chan is None or \
- self.telepathy_tubes_chan is None:
- return
-
- _logger.debug('%r: finished setting up tubes', self)
- reply_handler()
-
- def tubes_chan_ready(chan):
- _logger.debug('%r: Tubes channel %r is ready', self, chan)
- self.telepathy_tubes_chan = chan
- tubes_ready()
-
- def text_chan_ready(chan):
- _logger.debug('%r: Text channel %r is ready', self, chan)
- self.telepathy_text_chan = chan
- tubes_ready()
-
- def conn_ready(conn):
- _logger.debug('%r: Connection %r is ready', self, conn)
- self.telepathy_conn = conn
- found_text_channel = False
- found_tubes_channel = False
-
- for chan_path, chan_iface, handle_type, handle in chans:
- if handle_type != telepathy.HANDLE_TYPE_ROOM:
- return
-
- if chan_iface == telepathy.CHANNEL_TYPE_TEXT:
- telepathy.client.Channel(
- conn.service_name, chan_path,
- ready_handler=text_chan_ready,
- error_handler=error_handler)
- found_text_channel = True
- self.telepathy_room_handle = handle
-
- elif chan_iface == telepathy.CHANNEL_TYPE_TUBES:
- telepathy.client.Channel(
- conn.service_name, chan_path,
- ready_handler=tubes_chan_ready,
- error_handler=error_handler)
- found_tubes_channel = True
-
- if not found_text_channel:
- error_handler(AssertionError("Presence Service didn't create "
- "a chatroom"))
- elif not found_tubes_channel:
- error_handler(AssertionError("Presence Service didn't create "
- "tubes channel"))
-
- def channels_listed(bus_name, conn_path, channels):
- _logger.debug('%r: Connection on %s at %s, channels: %r',
- self, bus_name, conn_path, channels)
-
- # can't use assignment for this due to Python scoping
- chans.extend(channels)
-
- telepathy.client.Connection(bus_name, conn_path,
- ready_handler=conn_ready,
- error_handler=error_handler)
-
-
- self._activity.ListChannels(reply_handler=channels_listed,
- error_handler=error_handler)
-
- def _join_cb(self):
- _logger.debug('%r: Join finished', self)
+ self._resolve_handles(members, reply_cb=self._add_initial_buddies)
+
+ def _resolve_handles(self, input_handles, reply_cb):
+ def get_handle_owners_cb(handles):
+ self.telepathy_conn.InspectHandles(HANDLE_TYPE_CONTACT, handles,
+ reply_handler=reply_cb,
+ error_handler=self.__error_handler_cb,
+ dbus_interface=CONNECTION)
+
+ if self._text_channel_group_flags & \
+ CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
+
+ group = self.telepathy_text_chan[CHANNEL_INTERFACE_GROUP]
+ group.GetHandleOwners(input_handles,
+ reply_handler=get_handle_owners_cb,
+ error_handler=self.__error_handler_cb)
+ else:
+ get_handle_owners_cb(input_handles)
+
+ def _add_initial_buddies(self, contact_ids):
+ _logger.debug('__add_initial_buddies %r', contact_ids)
+ for contact_id in contact_ids:
+ self._buddies[contact_id] = self._get_buddy(contact_id)
+ # Once we have the initial members, we can finish the join process
self._joined = True
- self.emit("joined", True, None)
-
- def _join_error_cb(self, err):
- _logger.debug('%r: Join failed because: %s', self, err)
- self.emit("joined", False, str(err))
+ self.emit('joined', True, None)
+
+ def __text_channel_members_changed_cb(self, message, added, removed,
+ local_pending, remote_pending,
+ actor, reason):
+ _logger.debug('__text_channel_members_changed_cb %r',
+ [added, message, added, removed, local_pending,
+ remote_pending, actor, reason])
+ if self._channel_self_handle in added:
+ added.remove(self._channel_self_handle)
+ if added:
+ self._resolve_handles(added, reply_cb=self._add_buddies)
+
+ if self._channel_self_handle in removed:
+ removed.remove(self._channel_self_handle)
+ if removed:
+ self._resolve_handles(added, reply_cb=self._remove_buddies)
+
+ def _add_buddies(self, contact_ids):
+ for contact_id in contact_ids:
+ if contact_id not in self._buddies:
+ buddy = self._get_buddy(contact_id)
+ self.emit('buddy-joined', buddy)
+ self._buddies[contact_id] = buddy
+
+ def _remove_buddies(self, contact_ids):
+ for contact_id in contact_ids:
+ if contact_id in self._buddies:
+ buddy = self._get_buddy(contact_id)
+ self.emit('buddy-left', buddy)
+ del self._buddies[contact_id]
+
+ def _get_buddy(self, contact_id):
+ if contact_id in self._buddies:
+ return self._buddies[contact_id]
+ else:
+ return Buddy(self._account_path, contact_id)
def join(self):
"""Join this activity.
Emits 'joined' and otherwise does nothing if we're already joined.
"""
+ if self._join_command is not None:
+ return
+
if self._joined:
- self.emit("joined", True, None)
+ self.emit('joined', True, None)
return
_logger.debug('%r: joining', self)
- def joined():
- self.set_up_tubes(reply_handler=self._join_cb,
- error_handler=self._join_error_cb)
-
- self._activity.Join(reply_handler=joined,
- error_handler=self._join_error_cb)
+ self._join_command = _JoinCommand(self.telepathy_conn,
+ self.room_handle)
+ self._join_command.connect('finished', self.__joined_cb)
+ self._join_command.run()
+
+ def share(self, share_activity_cb, share_activity_error_cb):
+ if not self.room_handle is None:
+ raise ValueError('Already have a room handle')
+
+ self._share_command = _ShareCommand(self.telepathy_conn, self._id)
+ self._share_command.connect('finished',
+ partial(self.__shared_cb,
+ share_activity_cb,
+ share_activity_error_cb))
+ self._share_command.run()
+
+ def __shared_cb(self, share_activity_cb, share_activity_error_cb,
+ share_command, error):
+ _logger.debug('%r: Share finished %r', self, error)
+ if error is None:
+ self._joined = True
+ self.room_handle = share_command.room_handle
+ self.telepathy_text_chan = share_command.text_channel
+ self.telepathy_tubes_chan = share_command.tubes_channel
+ self._channel_self_handle = share_command.channel_self_handle
+ self._text_channel_group_flags = \
+ share_command.text_channel_group_flags
+ self._publish_properties()
+ self._start_tracking_properties()
+ self._start_tracking_buddies()
+ self._start_tracking_channel()
+ share_activity_cb(self)
+ else:
+ share_activity_error_cb(self, error)
+
+ def _publish_properties(self):
+ properties = {}
+
+ if self._color is not None:
+ properties['color'] = str(self._color)
+ if self._name is not None:
+ properties['name'] = str(self._name)
+ if self._type is not None:
+ properties['type'] = self._type
+ if self._tags is not None:
+ properties['tags'] = self._tags
+ properties['private'] = self._private
+
+ self.telepathy_conn.SetProperties(
+ self.room_handle,
+ properties,
+ dbus_interface=CONN_INTERFACE_ACTIVITY_PROPERTIES)
+
+ def __share_error_cb(self, share_activity_error_cb, error):
+ logging.debug('%r: Share failed because: %s', self, error)
+ share_activity_error_cb(self, error)
# GetChannels() wrapper
@@ -390,24 +444,265 @@ class Activity(gobject.GObject):
- a list of D-Bus object paths representing the channels
associated with this activity
"""
- (bus_name, connection, channels) = self._activity.GetChannels()
+ bus_name = self.telepathy_conn.requested_bus_name
+ connection_path = self.telepathy_conn.object_path
+ channels = [self.telepathy_text_chan.object_path,
+ self.telepathy_tubes_chan.object_path]
+
_logger.debug('%r: bus name is %s, connection is %s, channels are %r',
- self, bus_name, connection, channels)
- return bus_name, connection, channels
+ self, bus_name, connection_path, channels)
+ return bus_name, connection_path, channels
# Leaving
-
- def _leave_cb(self):
- """Callback for async action of leaving shared activity."""
+ def __text_channel_closed_cb(self):
+ self._joined = False
self.emit("joined", False, "left activity")
- def _leave_error_cb(self, err):
- """Callback for error in async leaving of shared activity."""
- _logger.debug('Failed to leave activity: %s', err)
-
def leave(self):
"""Leave this shared activity"""
_logger.debug('%r: leaving', self)
- self._joined = False
- self._activity.Leave(reply_handler=self._leave_cb,
- error_handler=self._leave_error_cb)
+ self.telepathy_text_chan.Close()
+
+class _BaseCommand(gobject.GObject):
+ __gsignals__ = {
+ 'finished': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ }
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self.text_channel = None
+ self.text_channel_group_flags = None
+ self.tubes_channel = None
+ self.room_handle = None
+ self.channel_self_handle = None
+
+ def run(self):
+ raise NotImplementedError()
+
+
+class _ShareCommand(_BaseCommand):
+ def __init__(self, connection, activity_id):
+ _BaseCommand.__init__(self)
+
+ self._connection = connection
+ self._activity_id = activity_id
+ self._finished = False
+ self._join_command = None
+
+ def run(self):
+ self._connection.RequestHandles(
+ HANDLE_TYPE_ROOM,
+ [self._activity_id],
+ reply_handler=self.__got_handles_cb,
+ error_handler=self.__error_handler_cb,
+ dbus_interface=CONNECTION)
+
+ def __got_handles_cb(self, handles):
+ logging.debug('__got_handles_cb %r', handles)
+ self.room_handle = handles[0]
+
+ self._join_command = _JoinCommand(self._connection, self.room_handle)
+ self._join_command.connect('finished', self.__joined_cb)
+ self._join_command.run()
+
+ def __joined_cb(self, join_command, error):
+ _logger.debug('%r: Join finished %r', self, error)
+ if error is not None:
+ self._finished = True
+ self.emit('finished', error)
+ return
+
+ self.text_channel = join_command.text_channel
+ self.text_channel_group_flags = join_command.text_channel_group_flags
+ self.tubes_channel = join_command.tubes_channel
+
+ self._connection.AddActivity(
+ self._activity_id,
+ self.room_handle,
+ reply_handler=self.__added_activity_cb,
+ error_handler=self.__error_handler_cb,
+ dbus_interface=CONN_INTERFACE_BUDDY_INFO)
+
+ def __added_activity_cb(self):
+ self._finished = True
+ self.emit('finished', None)
+
+ def __error_handler_cb(self, error):
+ self._finished = True
+ self.emit('finished', error)
+
+class _JoinCommand(_BaseCommand):
+ def __init__(self, connection, room_handle):
+ _BaseCommand.__init__(self)
+
+ self._connection = connection
+ self._finished = False
+ self.room_handle = room_handle
+ self._global_self_handle = None
+
+ def run(self):
+ if self._finished:
+ raise RuntimeError('This command has already finished')
+
+ self._connection.Get(CONNECTION, 'SelfHandle',
+ reply_handler=self.__get_self_handle_cb,
+ error_handler=self.__error_handler_cb,
+ dbus_interface=PROPERTIES_IFACE)
+
+ def __get_self_handle_cb(self, handle):
+ self._global_self_handle = handle
+
+ self._connection.RequestChannel(CHANNEL_TYPE_TEXT,
+ HANDLE_TYPE_ROOM, self.room_handle, True,
+ reply_handler=self.__create_text_channel_cb,
+ error_handler=self.__error_handler_cb,
+ dbus_interface=CONNECTION)
+
+ self._connection.RequestChannel(CHANNEL_TYPE_TUBES,
+ HANDLE_TYPE_ROOM, self.room_handle, True,
+ reply_handler=self.__create_tubes_channel_cb,
+ error_handler=self.__error_handler_cb,
+ dbus_interface=CONNECTION)
+
+ def __create_text_channel_cb(self, channel_path):
+ Channel(self._connection.requested_bus_name, channel_path,
+ ready_handler=self.__text_channel_ready_cb)
+
+ def __create_tubes_channel_cb(self, channel_path):
+ Channel(self._connection.requested_bus_name, channel_path,
+ ready_handler=self.__tubes_channel_ready_cb)
+
+ def __error_handler_cb(self, error):
+ self._finished = True
+ self.emit('finished', error)
+
+ def __tubes_channel_ready_cb(self, channel):
+ _logger.debug('%r: Tubes channel %r is ready', self, channel)
+ self.tubes_channel = channel
+ self._tubes_ready()
+
+ def __text_channel_ready_cb(self, channel):
+ _logger.debug('%r: Text channel %r is ready', self, channel)
+ self.text_channel = channel
+ self._tubes_ready()
+
+ def _tubes_ready(self):
+ if self.text_channel is None or \
+ self.tubes_channel is None:
+ return
+
+ _logger.debug('%r: finished setting up tubes', self)
+
+ self._add_self_to_channel()
+
+ def __text_channel_group_flags_changed_cb(self, added, removed):
+ _logger.debug('__text_channel_group_flags_changed_cb %r %r', added,
+ removed)
+ self.text_channel_group_flags |= added
+ self.text_channel_group_flags &= ~removed
+
+ def _add_self_to_channel(self):
+ # FIXME: cope with non-Group channels here if we want to support
+ # non-OLPC-compatible IMs
+
+ group = self.text_channel[CHANNEL_INTERFACE_GROUP]
+
+ def got_all_members(members, local_pending, remote_pending):
+ _logger.debug('got_all_members members %r local_pending %r '
+ 'remote_pending %r', members, local_pending,
+ remote_pending)
+
+ if self.text_channel_group_flags & \
+ CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
+ self_handle = self.channel_self_handle
+ else:
+ self_handle = self._global_self_handle
+
+ if self_handle in local_pending:
+ _logger.debug('%r: We are in local pending - entering', self)
+ group.AddMembers([self_handle], '',
+ reply_handler=lambda: None,
+ error_handler=lambda e: self._join_failed_cb(e,
+ 'got_all_members AddMembers'))
+
+ if members:
+ self.__text_channel_members_changed_cb('', members, (),
+ (), (), 0, 0)
+
+ def got_group_flags(flags):
+ self.text_channel_group_flags = flags
+ # by the time we hook this, we need to know the group flags
+ group.connect_to_signal('MembersChanged',
+ self.__text_channel_members_changed_cb)
+
+ # bootstrap by getting the current state. This is where we find
+ # out whether anyone was lying to us in their PEP info
+ group.GetAllMembers(reply_handler=got_all_members,
+ error_handler=self.__error_handler_cb)
+
+ def got_self_handle(channel_self_handle):
+ self.channel_self_handle = channel_self_handle
+ group.connect_to_signal('GroupFlagsChanged',
+ self.__text_channel_group_flags_changed_cb)
+ group.GetGroupFlags(reply_handler=got_group_flags,
+ error_handler=self.__error_handler_cb)
+
+ group.GetSelfHandle(reply_handler=got_self_handle,
+ error_handler=self.__error_handler_cb)
+
+ def __text_channel_members_changed_cb(self, message, added, removed,
+ local_pending, remote_pending,
+ actor, reason):
+ _logger.debug('__text_channel_members_changed_cb added %r removed %r '
+ 'local_pending %r remote_pending %r channel_self_handle '
+ '%r', added, removed, local_pending, remote_pending,
+ self.channel_self_handle)
+
+ if self.text_channel_group_flags & \
+ CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
+ self_handle = self.channel_self_handle
+ else:
+ self_handle = self._global_self_handle
+
+ if self_handle in added:
+ if PROPERTIES_INTERFACE not in self.text_channel:
+ self._finished = True
+ self.emit('finished', None)
+ else:
+ self.text_channel[PROPERTIES_INTERFACE].ListProperties(
+ reply_handler=self.__list_properties_cb,
+ error_handler=self.__error_handler_cb)
+
+ def __list_properties_cb(self, prop_specs):
+ # FIXME: invite-only ought to be set on private activities; but
+ # since only the owner can change invite-only, that would break
+ # activity scope changes.
+ props = {
+ 'anonymous': False, # otherwise buddy resolution breaks
+ 'invite-only': False, # anyone who knows about the channel can join
+ 'invite-restricted': False, # so non-owners can invite others
+ 'persistent': False, # vanish when there are no members
+ 'private': True, # don't appear in server room lists
+ }
+ props_to_set = []
+ for ident, name, sig_, flags in prop_specs:
+ value = props.pop(name, None)
+ if value is not None:
+ if flags & PROPERTY_FLAG_WRITE:
+ props_to_set.append((ident, value))
+ # FIXME: else error, but only if we're creating the room?
+ # FIXME: if props is nonempty, then we want to set props that aren't
+ # supported here - raise an error?
+
+ if props_to_set:
+ self.text_channel[PROPERTIES_INTERFACE].SetProperties(
+ props_to_set, reply_handler=self.__set_properties_cb,
+ error_handler=self.__error_handler_cb)
+ else:
+ self._finished = True
+ self.emit('finished', None)
+
+ def __set_properties_cb(self):
+ self._finished = True
+ self.emit('finished', None)
diff --git a/src/sugar/presence/buddy.py b/src/sugar/presence/buddy.py
index 2978e4d..0a72a36 100644
--- a/src/sugar/presence/buddy.py
+++ b/src/sugar/presence/buddy.py
@@ -1,4 +1,5 @@
# Copyright (C) 2007, Red Hat, Inc.
+# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -20,12 +21,25 @@
STABLE.
"""
+import logging
+
import gobject
-import gtk
import dbus
+import gconf
+from telepathy.interfaces import CONNECTION, \
+ CONNECTION_INTERFACE_ALIASING, \
+ CONNECTION_INTERFACE_CONTACTS
+from telepathy.constants import HANDLE_TYPE_CONTACT
+
+from sugar.presence.connectionmanager import get_connection_manager
+
+ACCOUNT_MANAGER_SERVICE = 'org.freedesktop.Telepathy.AccountManager'
+CONN_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo'
+_logger = logging.getLogger('sugar.presence.buddy')
-class Buddy(gobject.GObject):
+
+class BaseBuddy(gobject.GObject):
"""UI interface for a Buddy in the presence service
Each buddy interface tracks a set of activities and properties
@@ -38,12 +52,11 @@ class Buddy(gobject.GObject):
'color': color (XXX what format),
'current-activity': (XXX dbus path?),
'owner': (XXX dbus path?),
- 'icon': (XXX pixel data for an icon?)
- See __gproperties__
"""
+ __gtype_name__ = 'PresenceBaseBuddy'
+
__gsignals__ = {
- 'icon-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])),
'joined-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
([gobject.TYPE_PYOBJECT])),
'left-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
@@ -52,192 +65,182 @@ class Buddy(gobject.GObject):
([gobject.TYPE_PYOBJECT])),
}
- __gproperties__ = {
- 'key': (str, None, None, None, gobject.PARAM_READABLE),
- 'icon': (str, None, None, None, gobject.PARAM_READABLE),
- 'nick': (str, None, None, None, gobject.PARAM_READABLE),
- 'color': (str, None, None, None, gobject.PARAM_READABLE),
- 'current-activity': (object, None, None, gobject.PARAM_READABLE),
- 'owner': (bool, None, None, False, gobject.PARAM_READABLE),
- 'ip4-address': (str, None, None, None, gobject.PARAM_READABLE),
- 'tags': (str, None, None, None, gobject.PARAM_READABLE),
- }
+ def __init__(self):
+ gobject.GObject.__init__(self)
- _PRESENCE_SERVICE = "org.laptop.Sugar.Presence"
- _BUDDY_DBUS_INTERFACE = "org.laptop.Sugar.Presence.Buddy"
+ self._key = None
+ self._nick = None
+ self._color = None
+ self._current_activity = None
+ self._owner = False
+ self._ip4_address = None
+ self._tags = None
- def __init__(self, bus, new_obj_cb, del_obj_cb, object_path):
- """Initialise the reference to the buddy
+ def get_key(self):
+ return self._key
- bus -- dbus bus object
- new_obj_cb -- callback to call when this buddy joins an activity
- del_obj_cb -- callback to call when this buddy leaves an activity
- object_path -- path to the buddy object
- """
- gobject.GObject.__init__(self)
- self._object_path = object_path
- self._ps_new_object = new_obj_cb
- self._ps_del_object = del_obj_cb
- self._properties = {}
- self._activities = {}
-
- bobj = bus.get_object(self._PRESENCE_SERVICE, object_path)
- self._buddy = dbus.Interface(bobj, self._BUDDY_DBUS_INTERFACE)
-
- self._icon_changed_signal = self._buddy.connect_to_signal(
- 'IconChanged', self._icon_changed_cb, byte_arrays=True)
- self._joined_activity_signal = self._buddy.connect_to_signal(
- 'JoinedActivity', self._joined_activity_cb)
- self._left_activity_signal = self._buddy.connect_to_signal(
- 'LeftActivity', self._left_activity_cb)
- self._property_changed_signal = self._buddy.connect_to_signal(
- 'PropertyChanged', self._property_changed_cb)
-
- self._properties = self._get_properties_helper()
-
- activities = self._buddy.GetJoinedActivities()
- for op in activities:
- self._activities[op] = self._ps_new_object(op)
- self._icon = None
-
- def destroy(self):
- self._icon_changed_signal.remove()
- self._joined_activity_signal.remove()
- self._left_activity_signal.remove()
- self._property_changed_signal.remove()
-
- def _get_properties_helper(self):
- """Retrieve the Buddy's property dictionary from the service object
- """
- props = self._buddy.GetProperties(byte_arrays=True)
- if not props:
- return {}
- return props
+ def set_key(self, key):
+ self._key = key
- def do_get_property(self, pspec):
- """Retrieve a particular property from our property dictionary
-
- pspec -- XXX some sort of GTK specifier object with attributes
- including 'name', 'active' and 'icon-name'
- """
- if pspec.name == "key":
- return self._properties["key"]
- elif pspec.name == "nick":
- return self._properties["nick"]
- elif pspec.name == "color":
- return self._properties["color"]
- elif pspec.name == "tags":
- return self._properties["tags"]
- elif pspec.name == "current-activity":
- if not self._properties.has_key("current-activity"):
- return None
- curact = self._properties["current-activity"]
- if not len(curact):
- return None
- for activity in self._activities.values():
- if activity.props.id == curact:
- return activity
+ key = gobject.property(type=str, getter=get_key, setter=set_key)
+
+ def get_nick(self):
+ return self._nick
+
+ def set_nick(self, nick):
+ self._nick = nick
+
+ nick = gobject.property(type=str, getter=get_nick, setter=set_nick)
+
+ def get_color(self):
+ return self._color
+
+ def set_color(self, color):
+ self._color = color
+
+ color = gobject.property(type=str, getter=get_color, setter=set_color)
+
+ def get_current_activity(self):
+ if self._current_activity is None:
return None
- elif pspec.name == "owner":
- return self._properties["owner"]
- elif pspec.name == "icon":
- if not self._icon:
- self._icon = str(self._buddy.GetIcon(byte_arrays=True))
- return self._icon
- elif pspec.name == "ip4-address":
- # IPv4 address will go away quite soon
- if not self._properties.has_key("ip4-address"):
- return None
- return self._properties["ip4-address"]
+ for activity in self._activities.values():
+ if activity.props.id == self._current_activity:
+ return activity
+ return None
+
+ current_activity = gobject.property(type=object,
+ getter=get_current_activity)
+
+ def get_owner(self):
+ return self._owner
+
+ def set_owner(self, owner):
+ self._owner = owner
+
+ owner = gobject.property(type=bool, getter=get_owner, setter=set_owner,
+ default=False)
+
+ def get_ip4_address(self):
+ return self._ip4_address
+
+ def set_ip4_address(self, ip4_address):
+ self._ip4_address = ip4_address
+
+ ip4_address = gobject.property(type=str, getter=get_ip4_address,
+ setter=set_ip4_address)
+
+ def get_tags(self):
+ return self._tags
+
+ def set_tags(self, tags):
+ self._tags = tags
+
+ tags = gobject.property(type=str, getter=get_tags, setter=set_tags)
def object_path(self):
"""Retrieve our dbus object path"""
- return self._object_path
-
- def _emit_icon_changed_signal(self, icon_data):
- """Emit GObject signal when icon has changed"""
- self._icon = str(icon_data)
- self.emit('icon-changed')
- return False
-
- def _icon_changed_cb(self, icon_data):
- """Handle dbus signal by emitting a GObject signal"""
- gobject.idle_add(self._emit_icon_changed_signal, icon_data)
-
- def _emit_joined_activity_signal(self, object_path):
- """Emit activity joined signal with Activity object"""
- self.emit('joined-activity', self._ps_new_object(object_path))
- return False
-
- def _joined_activity_cb(self, object_path):
- """Handle dbus signal by emitting a GObject signal
-
- Stores the activity in activities dictionary as well
- """
- if not self._activities.has_key(object_path):
- self._activities[object_path] = self._ps_new_object(object_path)
- gobject.idle_add(self._emit_joined_activity_signal, object_path)
-
- def _emit_left_activity_signal(self, object_path):
- """Emit activity left signal with Activity object
-
- XXX this calls self._ps_new_object instead of self._ps_del_object,
- which would seem to be the incorrect callback?
- """
- self.emit('left-activity', self._ps_new_object(object_path))
- return False
-
- def _left_activity_cb(self, object_path):
- """Handle dbus signal by emitting a GObject signal
-
- Also removes from the activities dictionary
- """
- if self._activities.has_key(object_path):
- del self._activities[object_path]
- gobject.idle_add(self._emit_left_activity_signal, object_path)
-
- def _handle_property_changed_signal(self, prop_list):
- """Emit property-changed signal with property dictionary
-
- Generates a property-changed signal with the results of
- _get_properties_helper()
- """
- self._properties = self._get_properties_helper()
- # FIXME: don't leak unexposed property names
- self.emit('property-changed', prop_list)
- return False
-
- def _property_changed_cb(self, prop_list):
- """Handle dbus signal by emitting a GObject signal"""
- gobject.idle_add(self._handle_property_changed_signal, prop_list)
-
- def get_icon_pixbuf(self):
- """Retrieve Buddy's icon as a GTK pixel buffer
-
- XXX Why aren't the icons coming in as SVG?
- """
- if self.props.icon and len(self.props.icon):
- pbl = gtk.gdk.PixbufLoader()
- pbl.write(self.props.icon)
- pbl.close()
- return pbl.get_pixbuf()
- else:
- return None
+ return None
+
+
+class Buddy(BaseBuddy):
+ __gtype_name__ = 'PresenceBuddy'
+ def __init__(self, account_path, contact_id):
+ _logger.debug('Buddy.__init__')
+ BaseBuddy.__init__(self)
+
+ self._account_path = account_path
+ self.contact_id = contact_id
+ self.contact_handle = None
+
+ connection_manager = get_connection_manager()
+ connection = connection_manager.get_connection(account_path)
+
+ connection_name = connection.object_path.replace('/', '.')[1:]
+
+ bus = dbus.SessionBus()
+ obj = bus.get_object(connection_name, connection.object_path)
+ handles = obj.RequestHandles(HANDLE_TYPE_CONTACT, [self.contact_id],
+ dbus_interface=CONNECTION)
+ self.contact_handle = handles[0]
+
+ self._get_properties_call = bus.call_async(
+ connection_name,
+ connection.object_path,
+ CONN_INTERFACE_BUDDY_INFO,
+ 'GetProperties',
+ 'u',
+ (self.contact_handle,),
+ reply_handler=self.__got_properties_cb,
+ error_handler=self.__error_handler_cb,
+ utf8_strings=True,
+ byte_arrays=True)
+
+ self._get_attributes_call = bus.call_async(
+ connection_name,
+ connection.object_path,
+ CONNECTION_INTERFACE_CONTACTS,
+ 'GetContactAttributes',
+ 'auasb',
+ ([self.contact_handle], [CONNECTION_INTERFACE_ALIASING], False),
+ reply_handler=self.__got_attributes_cb,
+ error_handler=self.__error_handler_cb)
+
+ def __got_properties_cb(self, properties):
+ _logger.debug('__got_properties_cb %r', properties)
+ self._get_properties_call = None
+ self._update_properties(properties)
+
+ def __got_attributes_cb(self, attributes):
+ _logger.debug('__got_attributes_cb %r', attributes)
+ self._get_attributes_call = None
+ self._update_attributes(attributes[self.contact_handle])
+
+ def __error_handler_cb(self, error):
+ _logger.debug('__error_handler_cb %r', error)
+
+ def __properties_changed_cb(self, new_props):
+ _logger.debug('%r: Buddy properties changed to %r', self, new_props)
+ self._update_properties(new_props)
+
+ def _update_properties(self, properties):
+ if 'key' in properties:
+ self.props.key = properties['key']
+ if 'color' in properties:
+ self.props.color = properties['color']
+ if 'current-activity' in properties:
+ self.props.current_activity = properties['current-activity']
+ if 'owner' in properties:
+ self.props.owner = properties['owner']
+ if 'ip4-address' in properties:
+ self.props.ip4_address = properties['ip4-address']
+ if 'tags' in properties:
+ self.props.tags = properties['tags']
+
+ def _update_attributes(self, attributes):
+ nick_key = CONNECTION_INTERFACE_ALIASING + '/alias'
+ if nick_key in attributes:
+ self.props.nick = attributes[nick_key]
+
+ def do_get_property(self, pspec):
+ if pspec.name == 'nick' and self._get_attributes_call is not None:
+ _logger.debug('%r: Blocking on GetContactAttributes() because '
+ 'someone wants property nick', self)
+ self._get_attributes_call.block()
+ elif pspec.name != 'nick' and self._get_properties_call is not None:
+ _logger.debug('%r: Blocking on GetProperties() because someone '
+ 'wants property %s', self, pspec.name)
+ self._get_properties_call.block()
+
+ return BaseBuddy.do_get_property(self, pspec)
+
+
+class Owner(BaseBuddy):
+
+ __gtype_name__ = 'PresenceOwner'
+
+ def __init__(self):
+ BaseBuddy.__init__(self)
- def get_joined_activities(self):
- """Retrieve the set of all activities which this buddy has joined
-
- Uses the GetJoinedActivities method on the service
- object to produce object paths, wraps each in an
- Activity object.
-
- returns list of presence Activity objects
- """
- try:
- resp = self._buddy.GetJoinedActivities()
- except dbus.exceptions.DBusException:
- return []
- acts = []
- for item in resp:
- acts.append(self._ps_new_object(item))
- return acts
+ client = gconf.client_get_default()
+ self.props.nick = client.get_string("/desktop/sugar/user/nick")
+ self.props.color = client.get_string("/desktop/sugar/user/color")
diff --git a/src/sugar/presence/connectionmanager.py b/src/sugar/presence/connectionmanager.py
new file mode 100644
index 0000000..e681eb6
--- /dev/null
+++ b/src/sugar/presence/connectionmanager.py
@@ -0,0 +1,117 @@
+# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+"""
+UNSTABLE. It should really be internal to the sugar.presence package.
+"""
+
+from functools import partial
+
+import dbus
+from dbus import PROPERTIES_IFACE
+from telepathy.interfaces import ACCOUNT, \
+ ACCOUNT_MANAGER, \
+ CONNECTION
+from telepathy.constants import CONNECTION_STATUS_CONNECTED
+
+ACCOUNT_MANAGER_SERVICE = 'org.freedesktop.Telepathy.AccountManager'
+ACCOUNT_MANAGER_PATH = '/org/freedesktop/Telepathy/AccountManager'
+
+class Connection(object):
+ def __init__(self, account_path, connection):
+ self.account_path = account_path
+ self.connection = connection
+ self.connected = False
+
+class ConnectionManager(object):
+ """Track available telepathy connections"""
+
+ def __init__(self):
+ self._connections_per_account = {}
+
+ bus = dbus.SessionBus()
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH)
+ account_manager = dbus.Interface(obj, ACCOUNT_MANAGER)
+ account_paths = account_manager.Get(ACCOUNT_MANAGER, 'ValidAccounts',
+ dbus_interface=PROPERTIES_IFACE)
+ for account_path in account_paths:
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, account_path)
+ obj.connect_to_signal('AccountPropertyChanged',
+ partial(self.__account_property_changed_cb, account_path))
+ connection_path = obj.Get(ACCOUNT, 'Connection')
+ if connection_path != '/':
+ self._track_connection(account_path, connection_path)
+
+ def __account_property_changed_cb(self, account_path, properties):
+ if 'Connection' not in properties:
+ return
+ if properties['Connection'] == '/':
+ if account_path in self._connections_per_account:
+ del self._connections_per_account[account_path]
+ else:
+ self._track_connection(account_path, properties['Connection'])
+
+ def _track_connection(self, account_path, connection_path):
+ connection_name = connection_path.replace('/', '.')[1:]
+ bus = dbus.SessionBus()
+ connection = bus.get_object(connection_name, connection_path)
+ connection.connect_to_signal('StatusChanged',
+ partial(self.__status_changed_cb, account_path))
+ self._connections_per_account[account_path] = \
+ Connection(account_path, connection)
+
+ account = bus.get_object(ACCOUNT_MANAGER_SERVICE, account_path)
+ if account.Get(ACCOUNT, 'ConnectionStatus') == CONNECTION_STATUS_CONNECTED:
+ self._connections_per_account[account_path].connected = True
+ else:
+ self._connections_per_account[account_path].connected = False
+
+ def __status_changed_cb(self, account_path, status, reason):
+ if status == CONNECTION_STATUS_CONNECTED:
+ self._connections_per_account[account_path].connected = True
+ else:
+ self._connections_per_account[account_path].connected = False
+
+ def get_preferred_connection(self):
+ best_connection = None, None
+ for account_path, connection in self._connections_per_account.items():
+ if 'salut' in account_path and connection.connected:
+ best_connection = account_path, connection.connection
+ elif 'gabble' in account_path and connection.connected:
+ best_connection = account_path, connection.connection
+ break
+ return best_connection
+
+ def get_connection(self, account_path):
+ return self._connections_per_account[account_path].connection
+
+ def get_connections_per_account(self):
+ return self._connections_per_account
+
+ def get_account_for_connection(self, connection_path):
+ for account_path, connection in self._connections_per_account.items():
+ if connection.connection.object_path == connection_path:
+ return account_path
+ return None
+
+_connection_manager = None
+
+def get_connection_manager():
+ global _connection_manager
+ if not _connection_manager:
+ _connection_manager = ConnectionManager()
+ return _connection_manager
diff --git a/src/sugar/presence/presenceservice.py b/src/sugar/presence/presenceservice.py
index f4aa6df..862d6d0 100644
--- a/src/sugar/presence/presenceservice.py
+++ b/src/sugar/presence/presenceservice.py
@@ -1,4 +1,5 @@
# Copyright (C) 2007, Red Hat, Inc.
+# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -15,320 +16,47 @@
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
-"""UI class to access system-level presence object
-
+"""
STABLE.
"""
import logging
-import traceback
+import gobject
import dbus
import dbus.exceptions
import dbus.glib
-import gobject
+from dbus import PROPERTIES_IFACE
-from sugar.presence.buddy import Buddy
+from sugar.presence.buddy import Buddy, Owner
from sugar.presence.activity import Activity
+from sugar.presence.connectionmanager import get_connection_manager
-
-DBUS_SERVICE = "org.laptop.Sugar.Presence"
-DBUS_INTERFACE = "org.laptop.Sugar.Presence"
-DBUS_PATH = "/org/laptop/Sugar/Presence"
+from telepathy.interfaces import ACCOUNT, \
+ ACCOUNT_MANAGER, \
+ CONNECTION
+from telepathy.constants import HANDLE_TYPE_CONTACT
_logger = logging.getLogger('sugar.presence.presenceservice')
+ACCOUNT_MANAGER_SERVICE = 'org.freedesktop.Telepathy.AccountManager'
+ACCOUNT_MANAGER_PATH = '/org/freedesktop/Telepathy/AccountManager'
class PresenceService(gobject.GObject):
- """UI-side interface to the dbus presence service
-
- This class provides UI programmers with simplified access
- to the dbus service of the same name. It allows for observing
- various events from the presence service as GObject events,
- as well as some basic introspection queries.
- """
+ """Provides simplified access to the Telepathy framework to activities"""
__gsignals__ = {
- 'buddy-appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([gobject.TYPE_PYOBJECT])),
- 'buddy-disappeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([gobject.TYPE_PYOBJECT])),
- # parameters: (activity: Activity, inviter: Buddy, message: unicode)
- 'activity-invitation': (gobject.SIGNAL_RUN_FIRST, None, ([object]*3)),
- 'private-invitation': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
- gobject.TYPE_PYOBJECT, str])),
- 'activity-appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([gobject.TYPE_PYOBJECT])),
- 'activity-disappeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([gobject.TYPE_PYOBJECT])),
'activity-shared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
gobject.TYPE_PYOBJECT])),
}
- _PS_BUDDY_OP = DBUS_PATH + "/Buddies/"
- _PS_ACTIVITY_OP = DBUS_PATH + "/Activities/"
-
- def __init__(self, allow_offline_iface=True):
+ def __init__(self):
"""Initialise the service and attempt to connect to events
"""
gobject.GObject.__init__(self)
- self._objcache = {}
- self._joined = None
-
- # Get a connection to the session bus
- self._bus = dbus.SessionBus()
- self._bus.add_signal_receiver(self._name_owner_changed_cb,
- signal_name="NameOwnerChanged",
- dbus_interface="org.freedesktop.DBus")
-
- # attempt to load the interface to the service...
- self._allow_offline_iface = allow_offline_iface
- self._get_ps()
-
- def _name_owner_changed_cb(self, name, old, new):
- if name != DBUS_SERVICE:
- return
- if (old and len(old)) and (not new and not len(new)):
- # PS went away, clear out PS dbus service wrapper
- self._ps_ = None
- elif (not old and not len(old)) and (new and len(new)):
- # PS started up
- self._get_ps()
-
- _ps_ = None
-
- def _get_ps(self):
- """Retrieve dbus interface to PresenceService
-
- Also registers for updates from various dbus events on the
- interface.
-
- If unable to retrieve the interface, we will temporarily
- return an _OfflineInterface object to allow the calling
- code to continue functioning as though it had accessed a
- real presence service.
-
- If successful, caches the presence service interface
- for use by other methods and returns that interface
- """
- if not self._ps_:
- try:
- # NOTE: We need to follow_name_owner_changes here
- # because we can not connect to a signal unless
- # we follow the changes or we start the service
- # before we connect. Starting the service here
- # causes a major bottleneck during startup
- ps = dbus.Interface(
- self._bus.get_object(DBUS_SERVICE,
- DBUS_PATH,
- follow_name_owner_changes=True),
- DBUS_INTERFACE)
- except dbus.exceptions.DBusException, err:
- _logger.error(
- """Failure retrieving %r interface from
- the D-BUS service %r %r: %s""",
- DBUS_INTERFACE, DBUS_SERVICE, DBUS_PATH, err)
- if self._allow_offline_iface:
- return _OfflineInterface()
- raise RuntimeError('Failed to connect to the presence '
- 'service.')
- else:
- self._ps_ = ps
- ps.connect_to_signal('BuddyAppeared',
- self._buddy_appeared_cb)
- ps.connect_to_signal('BuddyDisappeared',
- self._buddy_disappeared_cb)
- ps.connect_to_signal('ActivityAppeared',
- self._activity_appeared_cb)
- ps.connect_to_signal('ActivityDisappeared',
- self._activity_disappeared_cb)
- ps.connect_to_signal('ActivityInvitation',
- self._activity_invitation_cb)
- ps.connect_to_signal('PrivateInvitation',
- self._private_invitation_cb)
- return self._ps_
-
- _ps = property(_get_ps, None, None,
- """DBUS interface to the PresenceService
- (services/presence/presenceservice)""")
-
- def _new_object(self, object_path):
- """Turn new object path into (cached) Buddy/Activity instance
-
- object_path -- full dbus path of the new object, must be
- prefixed with either of _PS_BUDDY_OP or _PS_ACTIVITY_OP
-
- Note that this method is called throughout the class whenever
- the representation of the object is required, it is not only
- called when the object is first discovered. The point is to only have
- _one_ Python object for any D-Bus object represented by an object path,
- effectively wrapping the D-Bus object in a single Python GObject.
-
- returns presence Buddy or Activity representation
- """
- obj = None
- try:
- obj = self._objcache[object_path]
- _logger.debug('Reused proxy %r', obj)
- except KeyError:
- if object_path.startswith(self._PS_BUDDY_OP):
- obj = Buddy(self._bus, self._new_object,
- self._del_object, object_path)
- elif object_path.startswith(self._PS_ACTIVITY_OP):
- obj = Activity(self._bus, self._new_object,
- self._del_object, object_path)
- try:
- # Pre-fill the activity's ID
- activity_id = obj.props.id
- except dbus.exceptions.DBusException:
- logging.debug('Cannot get the activity ID')
- else:
- raise RuntimeError("Unknown object type")
- self._objcache[object_path] = obj
- _logger.debug('Created proxy %r', obj)
- return obj
-
- def _have_object(self, object_path):
- return object_path in self._objcache.keys()
-
- def _del_object(self, object_path):
- """Fully remove an object from the object cache when
- it's no longer needed.
- """
- del self._objcache[object_path]
-
- def _emit_buddy_appeared_signal(self, object_path):
- """Emit GObject event with presence.buddy.Buddy object"""
- self.emit('buddy-appeared', self._new_object(object_path))
- return False
-
- def _buddy_appeared_cb(self, op):
- """Callback for dbus event (forwards to method to emit GObject
- event)"""
- gobject.idle_add(self._emit_buddy_appeared_signal, op)
-
- def _emit_buddy_disappeared_signal(self, object_path):
- """Emit GObject event with presence.buddy.Buddy object"""
- # Don't try to create a new object here if needed; it will probably
- # fail anyway because the object has already been destroyed in the PS
- if self._have_object(object_path):
- obj = self._objcache[object_path]
- self.emit('buddy-disappeared', obj)
-
- # We cannot maintain the object in the cache because that would
- # keep a lot of objects from being collected. That includes UI
- # objects due to signals using strong references.
- # If we want to cache some despite the memory usage increase,
- # we could use a LRU cache limited to some value.
- del self._objcache[object_path]
- obj.destroy()
-
- return False
-
- def _buddy_disappeared_cb(self, object_path):
- """Callback for dbus event (forwards to method to emit GObject
- event)"""
- gobject.idle_add(self._emit_buddy_disappeared_signal, object_path)
-
- def _emit_activity_invitation_signal(self, activity_path, buddy_path,
- message):
- """Emit GObject event with presence.activity.Activity object"""
- self.emit('activity-invitation', self._new_object(activity_path),
- self._new_object(buddy_path), unicode(message))
- return False
-
- def _activity_invitation_cb(self, activity_path, buddy_path, message):
- """Callback for dbus event (forwards to method to emit GObject
- event)"""
- gobject.idle_add(self._emit_activity_invitation_signal, activity_path,
- buddy_path, message)
-
- def _emit_private_invitation_signal(self, bus_name, connection,
- channel, chan_type):
- """Emit GObject event with bus_name, connection and channel"""
- self.emit('private-invitation', bus_name, connection,
- channel, chan_type)
- return False
-
- def _private_invitation_cb(self, bus_name, connection, channel, chan_type):
- """Callback for dbus event (forwards to method to emit GObject
- event)"""
- gobject.idle_add(self._emit_private_invitation_signal, bus_name,
- connection, channel, chan_type)
-
- def _emit_activity_appeared_signal(self, object_path):
- """Emit GObject event with presence.activity.Activity object"""
- self.emit('activity-appeared', self._new_object(object_path))
- return False
-
- def _activity_appeared_cb(self, object_path):
- """Callback for dbus event (forwards to method to emit GObject
- event)"""
- gobject.idle_add(self._emit_activity_appeared_signal, object_path)
-
- def _emit_activity_disappeared_signal(self, object_path):
- """Emit GObject event with presence.activity.Activity object"""
- self.emit('activity-disappeared', self._new_object(object_path))
- return False
-
- def _activity_disappeared_cb(self, object_path):
- """Callback for dbus event (forwards to method to emit GObject
- event)"""
- gobject.idle_add(self._emit_activity_disappeared_signal, object_path)
-
- def get(self, object_path):
- """Return the Buddy or Activity object corresponding to the given
- D-Bus object path.
- """
- return self._new_object(object_path)
- def get_activities(self):
- """Retrieve set of all activities from service
-
- returns list of Activity objects for all object paths
- the service reports exist (using GetActivities)
- """
- try:
- resp = self._ps.GetActivities()
- except dbus.exceptions.DBusException:
- _logger.exception('Unable to retrieve activity list from '
- 'presence service')
- return []
- else:
- acts = []
- for item in resp:
- acts.append(self._new_object(item))
- return acts
-
- def _get_activities_cb(self, reply_handler, resp):
- acts = []
- for item in resp:
- acts.append(self._new_object(item))
-
- reply_handler(acts)
-
- def _get_activities_error_cb(self, error_handler, e):
- if error_handler:
- error_handler(e)
- else:
- _logger.warn('Unable to retrieve activity-list from presence '
- 'service: %s', e)
-
- def get_activities_async(self, reply_handler=None, error_handler=None):
- """Retrieve set of all activities from service asyncronously
- """
-
- if not reply_handler:
- logging.error('Function get_activities_async called without' \
- 'a reply handler. Can not run.')
- return
-
- self._ps.GetActivities(
- reply_handler=lambda resp: \
- self._get_activities_cb(reply_handler, resp),
- error_handler=lambda e: \
- self._get_activities_error_cb(error_handler, e))
+ self._activity_cache = None
+ self._buddy_cache = {}
def get_activity(self, activity_id, warn_if_none=True):
"""Retrieve single Activity object for the given unique id
@@ -338,79 +66,64 @@ class PresenceService(gobject.GObject):
returns single Activity object or None if the activity
is not found using GetActivityById on the service
"""
- try:
- act_op = self._ps.GetActivityById(activity_id)
- except dbus.exceptions.DBusException, err:
- if warn_if_none:
- _logger.warn("Unable to retrieve activity handle for %r from "
- "presence service: %s", activity_id, err)
- return None
- return self._new_object(act_op)
-
- def get_buddies(self):
- """Retrieve set of all buddies from service
-
- returns list of Buddy objects for all object paths
- the service reports exist (using GetBuddies)
- """
- try:
- resp = self._ps.GetBuddies()
- except dbus.exceptions.DBusException:
- _logger.exception('Unable to retrieve buddy-list from presence '
- 'service')
- return []
+ if self._activity_cache is not None:
+ if self._activity_cache.props.id != activity_id:
+ raise RuntimeError('Activities can only access their own shared'
+ 'instance')
+ return self._activity_cache
else:
- buddies = []
- for item in resp:
- buddies.append(self._new_object(item))
- return buddies
-
- def _get_buddies_cb(self, reply_handler, resp):
- buddies = []
- for item in resp:
- buddies.append(self._new_object(item))
+ connection_manager = get_connection_manager()
+ connections_per_account = \
+ connection_manager.get_connections_per_account()
+ for account_path, connection in connections_per_account.items():
+ if not connection.connected:
+ continue
+ logging.debug("Calling GetActivity on %s", account_path)
+ try:
+ room_handle = connection.connection.GetActivity(activity_id)
+ except dbus.exceptions.DBusException, e:
+ name = 'org.freedesktop.Telepathy.Error.NotAvailable'
+ if e.get_dbus_name() == name:
+ logging.debug("There's no shared activity with the id "
+ "%s", activity_id)
+ else:
+ raise
+ else:
+ activity = Activity(account_path, connection.connection,
+ room_handle=room_handle)
+ self._activity_cache = activity
+ return activity
- reply_handler(buddies)
+ return None
- def _get_buddies_error_cb(self, error_handler, e):
- if error_handler:
- error_handler(e)
+ def get_activity_by_handle(self, connection_path, room_handle):
+ if self._activity_cache is not None:
+ if self._activity_cache.room_handle != room_handle:
+ raise RuntimeError('Activities can only access their own shared'
+ 'instance')
+ return self._activity_cache
else:
- _logger.warn('Unable to retrieve buddy-list from presence '
- 'service: %s', e)
-
- def get_buddies_async(self, reply_handler=None, error_handler=None):
- """Retrieve set of all buddies from service asyncronously
- """
-
- if not reply_handler:
- logging.error('Function get_buddies_async called without' \
- 'a reply handler. Can not run.')
- return
-
- self._ps.GetBuddies(
- reply_handler=lambda resp: \
- self._get_buddies_cb(reply_handler, resp),
- error_handler=lambda e: \
- self._get_buddies_error_cb(error_handler, e))
-
- def get_buddy(self, key):
- """Retrieve single Buddy object for the given public key
-
- key -- buddy's public encryption key
-
- returns single Buddy object or None if the activity
- is not found using GetBuddyByPublicKey on the
- service
- """
- try:
- buddy_op = self._ps.GetBuddyByPublicKey(dbus.ByteArray(key))
- except dbus.exceptions.DBusException:
- _logger.exception('Unable to retrieve buddy handle for %r from '
- 'presence service', key)
- return None
- return self._new_object(buddy_op)
-
+ connection_manager = get_connection_manager()
+ account_path = \
+ connection_manager.get_account_for_connection(connection_path)
+
+ connection_name = connection_path.replace('/', '.')[1:]
+ bus = dbus.SessionBus()
+ connection = bus.get_object(connection_name, connection_path)
+ activity = Activity(account_path, connection,
+ room_handle=room_handle)
+ self._activity_cache = activity
+ return activity
+
+ def get_buddy(self, account_path, contact_id):
+ if (account_path, contact_id) in self._buddy_cache:
+ return self._buddy_cache[(account_path, contact_id)]
+
+ buddy = Buddy(account_path, contact_id)
+ self._buddy_cache[(account_path, contact_id)] = buddy
+ return buddy
+
+ # DEPRECATED
def get_buddy_by_telepathy_handle(self, tp_conn_name, tp_conn_path,
handle):
"""Retrieve single Buddy object for the given public key
@@ -426,167 +139,114 @@ class PresenceService(gobject.GObject):
channel-specific handle.
:Returns: the Buddy object, or None if the buddy is not found
"""
- try:
- buddy_op = self._ps.GetBuddyByTelepathyHandle(tp_conn_name,
- tp_conn_path,
- handle)
- except dbus.exceptions.DBusException, err:
- _logger.warn('Unable to retrieve buddy handle for handle %u at '
- 'conn %s:%s from presence service: %s',
- handle, tp_conn_name, tp_conn_path, err)
- return None
- return self._new_object(buddy_op)
+
+ bus = dbus.Bus()
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH)
+ account_manager = dbus.Interface(obj, ACCOUNT_MANAGER)
+ account_paths = account_manager.Get(ACCOUNT_MANAGER, 'ValidAccounts',
+ dbus_interface=PROPERTIES_IFACE)
+ for account_path in account_paths:
+ obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, account_path)
+ connection_path = obj.Get(ACCOUNT, 'Connection')
+ if connection_path == tp_conn_path:
+ connection_name = connection_path.replace('/', '.')[1:]
+ connection = bus.get_object(connection_name, connection_path)
+ contact_ids = connection.InspectHandles(HANDLE_TYPE_CONTACT,
+ [handle],
+ dbus_interface=CONNECTION)
+ return self.get_buddy(account_path, contact_ids[0])
+
+ raise ValueError('Unknown buddy in connection %s with handle %d',
+ tp_conn_path, handle)
def get_owner(self):
- """Retrieves the laptop "owner" Buddy object."""
- try:
- owner_op = self._ps.GetOwner()
- except dbus.exceptions.DBusException:
- _logger.exception('Unable to retrieve local user/owner from '
- 'presence service')
- raise RuntimeError("Could not get owner object.")
- return self._new_object(owner_op)
-
- def _share_activity_cb(self, activity, op):
+ """Retrieves the laptop Buddy object."""
+ return Owner()
+
+ def __share_activity_cb(self, activity):
"""Finish sharing the activity
"""
- # FIXME find a better way to shutup pylint
- psact = self._new_object(op)
- psact._joined = True
- _logger.debug('%r: Just shared, setting up tubes', activity)
- psact.set_up_tubes(reply_handler=lambda:
- self.emit("activity-shared", True, psact, None),
- error_handler=lambda e:
- self._share_activity_error_cb(activity, e))
-
- def _share_activity_error_cb(self, activity, err):
- """Notify with GObject event of unsuccessful sharing of activity"""
- _logger.debug('Error sharing activity %s: %s', activity.get_id(),
- err)
- self.emit("activity-shared", False, None, err)
+ self.emit("activity-shared", True, activity, None)
- def share_activity(self, activity, properties=None, private=True):
- """Ask presence service to ask the activity to share itself publicly.
-
- Uses the AdvertiseActivity method on the service to ask for the
- sharing of the given activity. Arranges to emit activity-shared
- event with:
-
- (success, Activity, err)
-
- on success/failure.
-
- returns None
+ def __share_activity_error_cb(self, activity, error):
+ """Notify with GObject event of unsuccessful sharing of activity
"""
- actid = activity.get_id()
+ self.emit("activity-shared", False, activity, error)
+ def share_activity(self, activity, properties=None, private=True):
if properties is None:
properties = {}
- # Ensure the activity is not already shared/joined
- for obj in self._objcache.values():
- if not isinstance(object, Activity):
- continue
- if obj.props.id == actid or obj.props.joined:
- raise RuntimeError("Activity %s is already shared." %
- actid)
-
- atype = activity.get_bundle_id()
- name = activity.props.title
- properties['private'] = bool(private)
- self._ps.ShareActivity(actid, atype, name, properties,
- reply_handler=lambda op: \
- self._share_activity_cb(activity, op),
- error_handler=lambda e: \
- self._share_activity_error_cb(activity, e))
+ if 'id' not in properties:
+ properties['id'] = activity.get_id()
- def get_preferred_connection(self):
- """Gets the preferred telepathy connection object that an activity
- should use when talking directly to telepathy
-
- returns the bus name and the object path of the Telepathy connection"""
-
- try:
- bus_name, object_path = self._ps.GetPreferredConnection()
- except dbus.exceptions.DBusException:
- logging.error(traceback.format_exc())
- return None
+ if 'type' not in properties:
+ properties['type'] = activity.get_bundle_id()
- return bus_name, object_path
+ if 'name' not in properties:
+ properties['name'] = activity.metadata.get('title', None)
+ if 'color' not in properties:
+ properties['color'] = activity.metadata.get('icon-color', None)
-class _OfflineInterface(object):
- """Offline-presence-service interface
+ properties['private'] = private
- Used to mimic the behaviour of a real PresenceService sufficiently
- to avoid crashing client code that expects the given interface.
+ if self._activity_cache is not None:
+ raise ValueError('Activity %s is already tracked',
+ activity.get_id())
- XXX we could likely return a "MockOwner" object reasonably
- easily, but would it be worth it?
- """
+ connection_manager = get_connection_manager()
+ account_path, connection = \
+ connection_manager.get_preferred_connection()
- def raiseException(self, *args, **named):
- """Raise dbus.exceptions.DBusException"""
- raise dbus.exceptions.DBusException('PresenceService Interface not '
- 'available')
-
- GetActivities = raiseException
- GetActivityById = raiseException
- GetBuddies = raiseException
- GetBuddyByPublicKey = raiseException
- GetOwner = raiseException
- GetPreferredConnection = raiseException
+ if connection is None:
+ self.emit('activity-shared', False, None,
+ 'No active connection available')
+ return
- def ShareActivity(self, actid, atype, name, properties, reply_handler,
- error_handler):
- """Pretend to share and fail..."""
- exc = IOError('Unable to share activity as PresenceService is not '
- 'currently available')
- return error_handler(exc)
+ shared_activity = Activity(account_path, connection,
+ properties=properties)
+ self._activity_cache = shared_activity
+ if shared_activity.props.joined:
+ raise RuntimeError('Activity %s is already shared.' %
+ activity.props.id)
-class _MockPresenceService(gobject.GObject):
- """Test fixture allowing testing of items that use PresenceService
+ shared_activity.share(self.__share_activity_cb,
+ self.__share_activity_error_cb)
- See PresenceService for usage and purpose
- """
+ def get_preferred_connection(self):
+ """Gets the preferred telepathy connection object that an activity
+ should use when talking directly to telepathy
- __gsignals__ = {
- 'buddy-appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([gobject.TYPE_PYOBJECT])),
- 'buddy-disappeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([gobject.TYPE_PYOBJECT])),
- 'activity-invitation': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([gobject.TYPE_PYOBJECT])),
- 'private-invitation': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
- gobject.TYPE_PYOBJECT])),
- 'activity-appeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([gobject.TYPE_PYOBJECT])),
- 'activity-disappeared': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
- ([gobject.TYPE_PYOBJECT])),
- }
+ returns the bus name and the object path of the Telepathy connection
+ """
+ connection_manager = get_connection_manager()
+ account_path, connection = connection_manager.get_preferred_connection()
+ if connection is None:
+ return None
+ else:
+ return connection.requested_bus_name, connection.object_path
- def __init__(self):
- gobject.GObject.__init__(self)
+ # DEPRECATED
+ def get(self, object_path):
+ raise NotImplementedError()
+ # DEPRECATED
def get_activities(self):
- return []
+ raise NotImplementedError()
- def get_activity(self, activity_id):
- return None
+ # DEPRECATED
+ def get_activities_async(self, reply_handler=None, error_handler=None):
+ raise NotImplementedError()
+ # DEPRECATED
def get_buddies(self):
- return []
-
- def get_buddy(self, key):
- return None
-
- def get_owner(self):
- return None
+ raise NotImplementedError()
- def share_activity(self, activity, properties=None):
- return None
+ # DEPRECATED
+ def get_buddies_async(self, reply_handler=None, error_handler=None):
+ raise NotImplementedError()
_ps = None
@@ -596,5 +256,5 @@ def get_instance(allow_offline_iface=False):
"""Retrieve this process' view of the PresenceService"""
global _ps
if not _ps:
- _ps = PresenceService(allow_offline_iface)
+ _ps = PresenceService()
return _ps
diff --git a/src/sugar/profile.py b/src/sugar/profile.py
index 3ea1e67..2ed5aa6 100644
--- a/src/sugar/profile.py
+++ b/src/sugar/profile.py
@@ -53,8 +53,19 @@ class Profile(object):
self._pubkey = None
self._privkey_hash = None
- self.pubkey = self._load_pubkey()
- self.privkey_hash = self._hash_private_key()
+ def _get_pubkey(self):
+ if self._pubkey is None:
+ self._pubkey = self._load_pubkey()
+ return self._pubkey
+
+ pubkey = property(fget=_get_pubkey)
+
+ def _get_privkey_hash(self):
+ if self._privkey_hash is None:
+ self._privkey_hash = self._hash_private_key()
+ return self._privkey_hash
+
+ privkey_hash = property(fget=_get_privkey_hash)
def is_valid(self):
client = gconf.client_get_default()
@@ -105,14 +116,18 @@ class Profile(object):
return None
key = ""
+ begin_found = False
+ end_found = False
for l in lines:
l = l.strip()
if l.startswith("-----BEGIN DSA PRIVATE KEY-----"):
+ begin_found = True
continue
if l.startswith("-----END DSA PRIVATE KEY-----"):
+ end_found = True
continue
key += l
- if not len(key):
+ if not (len(key) and begin_found and end_found):
logging.error("Error parsing public key.")
return None
@@ -180,12 +195,16 @@ class Profile(object):
'# log files and features\n'\
'#export LM_DEBUG=net\n' \
'#export GABBLE_DEBUG=all\n' \
- '#export ' \
- 'GABBLE_LOGFILE=$HOME/.sugar/default/logs/telepathy-gabble.log\n' \
+ '#export GABBLE_LOGFILE=' \
+ '$HOME/.sugar/$SUGAR_PROFILE/logs/telepathy-gabble.log\n' \
'#export SALUT_DEBUG=all\n' \
- '#export ' \
- 'SALUT_LOGFILE=$HOME/.sugar/default/logs/telepathy-salut.log\n' \
+ '#export SALUT_LOGFILE=' \
+ '$HOME/.sugar/$SUGAR_PROFILE/logs/telepathy-salut.log\n' \
'#export GIBBER_DEBUG=all\n' \
+ '#export WOCKY_DEBUG=all\n' \
+ '#export MC_LOGFILE=' \
+ '$HOME/.sugar/$SUGAR_PROFILE/logs/mission-control.log\n' \
+ '#export MC_DEBUG=all\n' \
'#export PRESENCESERVICE_DEBUG=1\n' \
'#export SUGAR_LOGGER_LEVEL=debug\n\n' \
'# Uncomment the following line to enable core dumps\n' \
diff --git a/src/sugar/util.py b/src/sugar/util.py
index 7b33141..b947c0a 100644
--- a/src/sugar/util.py
+++ b/src/sugar/util.py
@@ -201,7 +201,7 @@ class LRU:
return iter(self.d)
def itervalues(self):
- for i, j in self.iteritems():
+ for i_, j in self.iteritems():
yield j
def keys(self):