diff --git a/configure.ac b/configure.ac index 6eebdfea..0ae1e154 100644 --- a/configure.ac +++ b/configure.ac @@ -115,6 +115,7 @@ services/presence2/Makefile services/clipboard/Makefile services/datastore/Makefile shell/Makefile +shell/intro/Makefile shell/data/Makefile shell/hardware/Makefile shell/view/Makefile diff --git a/services/presence2/Makefile.am b/services/presence2/Makefile.am index b0942db1..fbd3125c 100644 --- a/services/presence2/Makefile.am +++ b/services/presence2/Makefile.am @@ -4,7 +4,9 @@ sugar_PYTHON = \ activity.py \ buddy.py \ buddyiconcache.py \ - presenceservice.py + linklocal_plugin.py \ + presenceservice.py \ + server_plugin.py bin_SCRIPTS = sugar-presence-service2 diff --git a/services/presence2/buddy.py b/services/presence2/buddy.py index 63f6da9f..e010d19c 100644 --- a/services/presence2/buddy.py +++ b/services/presence2/buddy.py @@ -32,7 +32,7 @@ class Buddy(dbus.service.Object): """Represents another person on the network and keeps track of the activities and resources they make available for sharing.""" - def __init__(self, bus_name, object_id, icon_cache): + def __init__(self, bus_name, object_id, icon_cache, handle=None): if not bus_name: raise ValueError("DBus bus name must be valid") if not object_id or not isinstance(object_id, int): @@ -48,6 +48,8 @@ class Buddy(dbus.service.Object): self._icon_cache = icon_cache + self._handle = handle + self._nick_name = None self._color = None self._key = None diff --git a/services/presence2/linklocal_plugin.py b/services/presence2/linklocal_plugin.py new file mode 100644 index 00000000..01f2cf03 --- /dev/null +++ b/services/presence2/linklocal_plugin.py @@ -0,0 +1,23 @@ +# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2007, Collabora Ltd. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject + +class LinkLocalPlugin(gobject.GObject): + def __init__(self, registry): + gobject.GObject.__init__(self) + self._registry = registry diff --git a/services/presence2/presenceservice.py b/services/presence2/presenceservice.py index 532c77d9..2692f1aa 100644 --- a/services/presence2/presenceservice.py +++ b/services/presence2/presenceservice.py @@ -21,11 +21,12 @@ from telepathy.interfaces import (CONN_MGR_INTERFACE, CONN_INTERFACE) from telepathy.constants import (CONNECTION_STATUS_CONNECTING, CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED, CONNECTION_HANDLE_TYPE_CONTACT) -import telepathyclient +from server_plugin import ServerPlugin +from linklocal_plugin import LinkLocalPlugin + from buddy import Buddy, Owner from activity import Activity import buddyiconcache -from sugar import profile _PRESENCE_SERVICE = "org.laptop.Sugar.Presence" @@ -60,52 +61,23 @@ class PresenceService(dbus.service.Object): self._registry = ManagerRegistry() self._registry.LoadManagers() - account = { - 'account': 'olpc@collabora.co.uk', - 'password': 'learn', - 'server': 'light.bluelinux.co.uk' - } - self._server_client = self._connect_to_connection_manager("jabber", account) - self._handles[self._server_client] = {} + # Set up the server connection + self._server_plugin = ServerPlugin(self._registry) + self._handles[self._server_plugin] = {} - # Telepathy link local connection - self._ll_client = None + self._server_plugin.connect('status', self._server_status_cb) + self._server_plugin.connect('contact-online', self._contact_online) + self._server_plugin.connect('contact-offline', self._contact_offline) + self._server_plugin.start() - self._server_client.connect('contact-online', self._contact_online) - self._server_client.connect('contact-offline', self._contact_offline) - self._server_client.run() + # Set up the link local connection + self._ll_plugin = LinkLocalPlugin(self._registry) + self._handles[self._ll_plugin] = {} dbus.service.Object.__init__(self, self._bus_name, _PRESENCE_PATH) - def _connect_to_connection_manager(self, protocol, account): - if protocol == "jabber": - cm = "gabble" - else: - return - - mgr = self._registry.GetManager(cm) - conn = None - - # Search existing connections, if any, that we might be able to use - connections = Connection.get_connections() - for item in connections: - if item[CONN_INTERFACE].GetProtocol() != protocol: - continue - if not item.object_path.startswith("/org/freedesktop/Telepathy/Connection/%s/%s/" % (cm, protocol)): - continue - if item[CONN_INTERFACE].GetStatus() == CONNECTION_STATUS_CONNECTED: - self_name = account['account'] - test_handle = item[CONN_INTERFACE].RequestHandles(CONNECTION_HANDLE_TYPE_CONTACT, [self_name])[0] - if item[CONN_INTERFACE].GetSelfHandle() != test_handle: - continue - conn = item - - if not conn: - conn_bus_name, conn_object_path = \ - mgr[CONN_MGR_INTERFACE].RequestConnection(protocol, account) - conn = Connection(conn_bus_name, conn_object_path) - - return telepathyclient.TelepathyClient(conn) + def _server_status_cb(self, plugin, status, reason): + pass def _contact_online(self, tp, handle, key): buddy = self._buddies.get(key) @@ -113,7 +85,7 @@ class PresenceService(dbus.service.Object): if not buddy: # we don't know yet this buddy objid = self._get_next_object_id() - buddy = Buddy(self._bus_name, objid, self._icon_cache) + buddy = Buddy(self._bus_name, objid, self._icon_cache, handle=handle) buddy.set_key(key) print "create buddy" self._buddies[key] = buddy diff --git a/services/presence2/server_plugin.py b/services/presence2/server_plugin.py new file mode 100644 index 00000000..09ad1573 --- /dev/null +++ b/services/presence2/server_plugin.py @@ -0,0 +1,244 @@ +# Copyright (C) 2007, Red Hat, Inc. +# Copyright (C) 2007, Collabora Ltd. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject +from sugar import profile +from sugar import util +import logging + +from telepathy.client import ConnectionManager, ManagerRegistry, Connection, Channel +from telepathy.interfaces import ( + CONN_MGR_INTERFACE, CONN_INTERFACE, CHANNEL_TYPE_CONTACT_LIST, CHANNEL_INTERFACE_GROUP, CONN_INTERFACE_ALIASING, + CONN_INTERFACE_AVATARS, CONN_INTERFACE_PRESENCE) +from telepathy.constants import ( + CONNECTION_HANDLE_TYPE_NONE, CONNECTION_HANDLE_TYPE_CONTACT, + CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED, CONNECTION_STATUS_CONNECTING, + CONNECTION_HANDLE_TYPE_LIST, CONNECTION_HANDLE_TYPE_CONTACT, + CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED) + + +class ServerPlugin(gobject.GObject): + __gsignals__ = { + 'contact-online': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + 'contact-offline': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'status': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_INT, gobject.TYPE_INT])) + } + + def __init__(self, registry): + gobject.GObject.__init__(self) + + self._registry = registry + self._online_contacts = set() + self._account = self._get_account_info() + + self._ever_connected = False + self._conn = self._init_connection() + + def _get_account_info(self): + account_info = {'server': 'olpc.collabora.co.uk'} + + pubkey = profile.get_pubkey() + khash = util.printable_hash(util._sha_data(pubkey)) + account_info['account'] = "%s@%s" % (khash, account_info['server']) + + account_info['password'] = profile.get_private_key_hash() + return account_info + + def _init_connection(self, register=False): + protocol = 'jabber' + + mgr = self._registry.GetManager('gabble') + + # Search existing connections, if any, that we might be able to use + connections = Connection.get_connections() + conn = None + for item in connections: + if not item.object_path.startswith("/org/freedesktop/Telepathy/Connection/gabble/jabber/"): + continue + if item[CONN_INTERFACE].GetStatus() == CONNECTION_STATUS_DISCONNECTED: + item[CONN_INTERFACE].Disconnect() + continue + if item[CONN_INTERFACE].GetProtocol() != protocol: + continue + if item[CONN_INTERFACE].GetStatus() == CONNECTION_STATUS_CONNECTED: + self_name = self._account['account'] + test_handle = item[CONN_INTERFACE].RequestHandles(CONNECTION_HANDLE_TYPE_CONTACT, [self_name])[0] + if item[CONN_INTERFACE].GetSelfHandle() != test_handle: + continue + conn = item + break + + if not conn: + acct = self._account.copy() + if register: + acct['register'] = True + + # Create a new connection + print acct + name, path = mgr[CONN_MGR_INTERFACE].RequestConnection(protocol, acct) + conn = Connection(name, path) + + conn[CONN_INTERFACE].connect_to_signal('StatusChanged', self._status_changed_cb) + + # hack + conn._valid_interfaces.add(CONN_INTERFACE_PRESENCE) + conn[CONN_INTERFACE_PRESENCE].connect_to_signal('PresenceUpdate', + self._presence_update_cb) + + return conn + + def _request_list_channel(self, name): + handle = self._conn[CONN_INTERFACE].RequestHandles( + CONNECTION_HANDLE_TYPE_LIST, [name])[0] + chan_path = self._conn[CONN_INTERFACE].RequestChannel( + CHANNEL_TYPE_CONTACT_LIST, CONNECTION_HANDLE_TYPE_LIST, + handle, True) + channel = Channel(self._conn._dbus_object._named_service, chan_path) + # hack + channel._valid_interfaces.add(CHANNEL_INTERFACE_GROUP) + return channel + + def _connected_cb(self): + self._ever_connected = True + + # the group of contacts who may receive your presence + publish = self._request_list_channel('publish') + publish_handles, local_pending, remote_pending = publish[CHANNEL_INTERFACE_GROUP].GetAllMembers() + + # the group of contacts for whom you wish to receive presence + subscribe = self._request_list_channel('subscribe') + subscribe_handles = subscribe[CHANNEL_INTERFACE_GROUP].GetMembers() + + if local_pending: + # accept pending subscriptions + #print 'pending: %r' % local_pending + publish[CHANNEL_INTERFACE_GROUP].AddMembers(local_pending, '') + + not_subscribed = list(set(publish_handles) - set(subscribe_handles)) + self_handle = self._conn[CONN_INTERFACE].GetSelfHandle() + self._online_contacts.add(self_handle) + + for handle in not_subscribed: + # request subscriptions from people subscribed to us if we're not subscribed to them + subscribe[CHANNEL_INTERFACE_GROUP].AddMembers([self_handle], '') + + # hack + self._conn._valid_interfaces.add(CONN_INTERFACE_ALIASING) + + if CONN_INTERFACE_ALIASING in self._conn: + aliases = self._conn[CONN_INTERFACE_ALIASING].RequestAliases(subscribe_handles) + else: + aliases = self._conn[CONN_INTERFACE].InspectHandles(CONNECTION_HANDLE_TYPE_CONTACT, subscribe_handles) + + #for handle, alias in zip(subscribe_handles, aliases): + # print alias + # self.buddies[handle].alias = alias + + # hack + self._conn._valid_interfaces.add(CONN_INTERFACE_AVATARS) + + #if CONN_INTERFACE_AVATARS in self._conn: + # #tokens = self._conn[CONN_INTERFACE_AVATARS].RequestAvatarTokens(subscribe_handles) + + # #for handle, token in zip(subscribe_handles, tokens): + # for handle in subscribe_handles: + # avatar, mime_type = self._conn[CONN_INTERFACE_AVATARS].RequestAvatar(handle) + # self.buddies[handle].avatar = ''.join(map(chr, avatar)) + + # import gtk + # window = gtk.Window() + # window.set_title(self.buddies[handle].alias) + # loader = gtk.gdk.PixbufLoader() + # loader.write(self.buddies[handle].avatar) + # loader.close() + # image = gtk.Image() + # image.set_from_pixbuf(loader.get_pixbuf()) + # window.add(image) + # window.show_all() + + def _status_changed_cb(self, state, reason): + gobject.idle_add(self._status_changed_cb2, state, reason) + + def _status_changed_cb2(self, state, reason): + if state == CONNECTION_STATUS_CONNECTING: + print 'connecting: %r' % reason + elif state == CONNECTION_STATUS_CONNECTED: + print 'connected: %r' % reason + self.emit('status', state, int(reason)) + self._connected_cb() + elif state == CONNECTION_STATUS_DISCONNECTED: + print 'disconnected: %r' % reason + self.emit('status', state, int(reason)) + if reason == CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED and \ + not self._ever_connected: + # Hmm; probably aren't registered on the server, try reconnecting + # and registering + self.disconnect() + del self._conn + self._conn = self._init_connection(register=True) + self.start() + return False + + def start(self): + # If the connection is already connected query initial contacts + conn_status = self._conn[CONN_INTERFACE].GetStatus() + if conn_status == CONNECTION_STATUS_CONNECTED: + self._connected_cb() + subscribe = self._request_list_channel('subscribe') + subscribe_handles = subscribe[CHANNEL_INTERFACE_GROUP].GetMembers() + self._conn[CONN_INTERFACE_PRESENCE].RequestPresence(subscribe_handles) + elif conn_status == CONNECTION_STATUS_CONNECTING: + pass + else: + self._conn[CONN_INTERFACE].Connect() + + def disconnect(self): + self._conn[CONN_INTERFACE].Disconnect() + + def _contact_go_offline(self, handle): + name = self._conn[CONN_INTERFACE].InspectHandles(CONNECTION_HANDLE_TYPE_CONTACT, [handle])[0] + print name, "offline" + + self._online_contacts.remove(handle) + self.emit("contact-offline", handle) + + def _contact_go_online(self, handle): + name = self._conn[CONN_INTERFACE].InspectHandles(CONNECTION_HANDLE_TYPE_CONTACT, [handle])[0] + print name, "online" + + # TODO: use the OLPC interface to get the key + key = handle + + self._online_contacts.add(handle) + self.emit("contact-online", handle, key) + + def _presence_update_cb(self, presence): + for handle in presence: + timestamp, statuses = presence[handle] + + name = self._conn[CONN_INTERFACE].InspectHandles(CONNECTION_HANDLE_TYPE_CONTACT, [handle])[0] + online = handle in self._online_contacts + + for status, params in statuses.items(): + if not online and status in ["available", "away", "brb", "busy", "dnd", "xa"]: + self._contact_go_online(handle) + elif online and status in ["offline", "invisible"]: + self._contact_go_offline(handle) + diff --git a/services/presence2/telepathyclient.py b/services/presence2/telepathyclient.py deleted file mode 100644 index 34843bd6..00000000 --- a/services/presence2/telepathyclient.py +++ /dev/null @@ -1,194 +0,0 @@ - -import dbus.glib -import gobject - -from telepathy.client import ConnectionManager, ManagerRegistry, Connection, Channel -from telepathy.interfaces import ( - CONN_MGR_INTERFACE, CONN_INTERFACE, CHANNEL_TYPE_CONTACT_LIST, CHANNEL_INTERFACE_GROUP, CONN_INTERFACE_ALIASING, - CONN_INTERFACE_AVATARS, CONN_INTERFACE_PRESENCE) -from telepathy.constants import ( - CONNECTION_HANDLE_TYPE_NONE, CONNECTION_HANDLE_TYPE_CONTACT, - CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED, CONNECTION_STATUS_CONNECTING, - CONNECTION_HANDLE_TYPE_LIST, CONNECTION_HANDLE_TYPE_CONTACT) - -loop = None - -import buddy - -class TelepathyClient(gobject.GObject): - __gsignals__ = { - 'contact-online':(gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), - 'contact-offline':(gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([gobject.TYPE_PYOBJECT])), - } - - def __init__(self, conn): - gobject.GObject.__init__(self) - - self._online_contacts = set() - - conn[CONN_INTERFACE].connect_to_signal('StatusChanged', - self._status_changed_cb) - - # hack - conn._valid_interfaces.add(CONN_INTERFACE_PRESENCE) - conn[CONN_INTERFACE_PRESENCE].connect_to_signal('PresenceUpdate', - self._presence_update_cb) - - self.conn = conn - - def _request_list_channel(self, name): - handle = self.conn[CONN_INTERFACE].RequestHandles( - CONNECTION_HANDLE_TYPE_LIST, [name])[0] - chan_path = self.conn[CONN_INTERFACE].RequestChannel( - CHANNEL_TYPE_CONTACT_LIST, CONNECTION_HANDLE_TYPE_LIST, - handle, True) - channel = Channel(self.conn._dbus_object._named_service, chan_path) - # hack - channel._valid_interfaces.add(CHANNEL_INTERFACE_GROUP) - return channel - - def _connected_cb(self): - # the group of contacts who may receive your presence - publish = self._request_list_channel('publish') - publish_handles, local_pending, remote_pending = publish[CHANNEL_INTERFACE_GROUP].GetAllMembers() - - # the group of contacts for whom you wish to receive presence - subscribe = self._request_list_channel('subscribe') - subscribe_handles = subscribe[CHANNEL_INTERFACE_GROUP].GetMembers() - - if local_pending: - # accept pending subscriptions - #print 'pending: %r' % local_pending - publish[CHANNEL_INTERFACE_GROUP].AddMembers(local_pending, '') - - not_subscribed = list(set(publish_handles) - set(subscribe_handles)) - self_handle = self.conn[CONN_INTERFACE].GetSelfHandle() - self._online_contacts.add(self_handle) - - for handle in not_subscribed: - # request subscriptions from people subscribed to us if we're not subscribed to them - subscribe[CHANNEL_INTERFACE_GROUP].AddMembers([self_handle], '') - - # hack - self.conn._valid_interfaces.add(CONN_INTERFACE_ALIASING) - - if CONN_INTERFACE_ALIASING in self.conn: - aliases = self.conn[CONN_INTERFACE_ALIASING].RequestAliases(subscribe_handles) - else: - aliases = self.conn[CONN_INTERFACE].InspectHandles(CONNECTION_HANDLE_TYPE_CONTACT, subscribe_handles) - - #for handle, alias in zip(subscribe_handles, aliases): - # print alias - # self.buddies[handle].alias = alias - - # hack - self.conn._valid_interfaces.add(CONN_INTERFACE_AVATARS) - - #if CONN_INTERFACE_AVATARS in self.conn: - # #tokens = self.conn[CONN_INTERFACE_AVATARS].RequestAvatarTokens(subscribe_handles) - - # #for handle, token in zip(subscribe_handles, tokens): - # for handle in subscribe_handles: - # avatar, mime_type = self.conn[CONN_INTERFACE_AVATARS].RequestAvatar(handle) - # self.buddies[handle].avatar = ''.join(map(chr, avatar)) - - # import gtk - # window = gtk.Window() - # window.set_title(self.buddies[handle].alias) - # loader = gtk.gdk.PixbufLoader() - # loader.write(self.buddies[handle].avatar) - # loader.close() - # image = gtk.Image() - # image.set_from_pixbuf(loader.get_pixbuf()) - # window.add(image) - # window.show_all() - - def _status_changed_cb(self, state, reason): - if state == CONNECTION_STATUS_CONNECTING: - print 'connecting' - elif state == CONNECTION_STATUS_CONNECTED: - print 'connected' - self._connected_cb() - elif state == CONNECTION_STATUS_DISCONNECTED: - print 'disconnected' - loop.quit() - - def run(self): - # If the connection is already connected query initial contacts - conn_status = self.conn[CONN_INTERFACE].GetStatus() - if conn_status == CONNECTION_STATUS_CONNECTED: - self._connected_cb() - subscribe = self._request_list_channel('subscribe') - subscribe_handles = subscribe[CHANNEL_INTERFACE_GROUP].GetMembers() - self.conn[CONN_INTERFACE_PRESENCE].RequestPresence(subscribe_handles) - elif conn_status == CONNECTION_STATUS_CONNECTING: - pass - else: - self.conn[CONN_INTERFACE].Connect() - - def disconnect(self): - self.conn[CONN_INTERFACE].Disconnect() - - - def _contact_go_offline(self, handle): - name = self.conn[CONN_INTERFACE].InspectHandles(CONNECTION_HANDLE_TYPE_CONTACT, [handle])[0] - print name, "offline" - - self._online_contacts.remove(handle) - self.emit("contact-offline", handle) - - def _contact_go_online(self, handle): - name = self.conn[CONN_INTERFACE].InspectHandles(CONNECTION_HANDLE_TYPE_CONTACT, [handle])[0] - print name, "online" - - # TODO: use the OLPC interface to get the key - key = handle - - self._online_contacts.add(handle) - self.emit("contact-online", handle, key) - - def _presence_update_cb(self, presence): - for handle in presence: - timestamp, statuses = presence[handle] - - name = self.conn[CONN_INTERFACE].InspectHandles(CONNECTION_HANDLE_TYPE_CONTACT, [handle])[0] - online = handle in self._online_contacts - - for status, params in statuses.items(): - if not online and status in ["available", "away", "brb", "busy", "dnd", "xa"]: - self._contact_go_online(handle) - elif online and status in ["offline", "invisible"]: - self._contact_go_offline(handle) - -if __name__ == '__main__': - import logging - logging.basicConfig() - - registry = ManagerRegistry() - registry.LoadManagers() - mgr = registry.GetManager('gabble') - protocol = 'jabber' - account = { - 'account': 'olpc@collabora.co.uk', - 'password': 'learn', - 'server': 'light.bluelinux.co.uk' - } - loop = gobject.MainLoop() - conn_bus_name, conn_object_path = \ - mgr[CONN_MGR_INTERFACE].RequestConnection(protocol, account) - print conn_bus_name - print conn_object_path - conn = Connection(conn_bus_name, conn_object_path) - client = TelepathyClient(conn) - - try: - loop.run() - finally: - try: - #conn[CONN_INTERFACE].Disconnect() - client.disconnect() - except: - pass - diff --git a/shell/Makefile.am b/shell/Makefile.am index e5a566f5..c1185b30 100644 --- a/shell/Makefile.am +++ b/shell/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = data hardware model view +SUBDIRS = data hardware model view intro bin_SCRIPTS = \ sugar-activity \ diff --git a/shell/hardware/nmclient.py b/shell/hardware/nmclient.py index fa763ab6..3bcc4332 100644 --- a/shell/hardware/nmclient.py +++ b/shell/hardware/nmclient.py @@ -363,7 +363,7 @@ class NMClient(gobject.GObject): self._get_initial_devices() def get_devices(self): - return self._devices + return self._devices.values() def _get_initial_devices_reply_cb(self, ops): for op in ops: diff --git a/shell/intro/Makefile.am b/shell/intro/Makefile.am new file mode 100644 index 00000000..cc11aa90 --- /dev/null +++ b/shell/intro/Makefile.am @@ -0,0 +1,6 @@ +sugardir = $(pkgdatadir)/shell/intro +sugar_PYTHON = \ + __init__.py \ + colorpicker.py \ + intro.py \ + glive.py diff --git a/shell/intro/__init__.py b/shell/intro/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/shell/intro/colorpicker.py b/shell/intro/colorpicker.py new file mode 100644 index 00000000..84641244 --- /dev/null +++ b/shell/intro/colorpicker.py @@ -0,0 +1,213 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import hippo +import random, math +import gobject + +from sugar.graphics.canvasicon import CanvasIcon +from sugar.graphics import color +from sugar.graphics import units +from sugar.graphics.xocolor import XoColor + +# Ported from a JavaScript implementation (C) 2007 Jacob Rus +# http://www.hcs.harvard.edu/~jrus/olpc/colorpicker.svg + + +# An array of arrays with value 1-9 for each of 40 different hues, +# starting with 5R, and going all the way around to 2.5R. + +munsell_colors = [ + # 1 2 3 4 5 6 7 8 9 + ["#40011d","#640d28","#8e172e","#bf1837","#f50141","#fc5f68","#f49599","#f1bdc3","#f8dfe9"], # 5 R + ["#410117","#630f1f","#8d1c21","#bd2024","#f21d22","#fc6252","#f5968d","#f2bdbe","#f9dfe7"], # 7.5 R + ["#410210","#631214","#940c00","#b03716","#d84b18","#f96735","#f49781","#f3bdb8","#fadfe5"], # 10 R + ["#410405","#611601","#7c3111","#a14614","#c85b15","#f07015","#fd9465","#f2beb2","#fbdfe1"], # 2.5 YR + ["#381001","#542305","#763601","#944f1f","#ba651f","#e07b1d","#f69955","#febc98","#fcdfdc"], # 5 YR + ["#2b180e","#492a14","#6b3e14","#8e5313","#b26a0a","#d1832b","#f69b26","#f9be91","#fbe0d7"], # 7.5 YR + ["#2a190b","#462c0f","#66400c","#885703","#a67020","#c9881a","#eba004","#fcbf6f","#f9e1d3"], # 10 YR + ["#271a09","#422e09","#614303","#7c5c1e","#9e7415","#bb8e32","#dda72c","#ffc01e","#f6e2d0"], # 2.5 Y + ["#251b09","#3f2f06","#574621","#775e19","#967709","#b2922a","#d2ac1d","#edc73f","#fae3b2"], # 5 Y + ["#221c0a","#3b3105","#534820","#706116","#8b7b2e","#a99525","#c8af13","#e3ca3a","#fde676"], # 7.5 Y + ["#1f1d0d","#363207","#4e4920","#6a6316","#857d2f","#a19825","#beb30e","#dacd39","#f8ea20"], # 10 Y + ["#1c1e11","#31340e","#474c02","#61651c","#7b810b","#959b2b","#b0b71b","#ccd23f","#e8ee25"], # 2.5 GY + ["#1a1e13","#2b3515","#3e4e0f","#576824","#6e841e","#85a007","#a1ba30","#b9d719","#d6f337"], # 5 GY + ["#082205","#18390a","#255301","#3e6d19","#508914","#61a704","#7ec22e","#90e01a","#abfc39"], # 7.5 GY + ["#01230d","#0b3a18","#0a541b","#2a6f2d","#048f1e","#0bad21","#15cb23","#0cea1c","#91fe81"], # 10 GY + ["#141f1a","#183728","#115336","#2b6d4c","#2e8b5b","#2ba96a","#20c877","#4be48e","#83feaf"], # 2.5 G + ["#131f1b","#15382c","#285041","#226d54","#138c68","#3ea681","#33c695","#54e2ae","#8afbcc"], # 5 G + ["#121f1c","#11382f","#245045","#176e5a","#3c8874","#33a78a","#1ac6a0","#46e2b9","#81fbd6"], # 7.5 G + ["#111f1e","#0d3832","#205049","#0a6e60","#38887a","#26a792","#4ec2ac","#38e2c4","#79fbe0"], # 10 G + ["#0f1f1f","#083836","#1c504d","#366a66","#338880","#13a79a","#45c2b4","#24e2ce","#72fbe9"], # 2.5 BG + ["#0e1f22","#03383b","#185053","#34696c","#2d8788","#49a3a2","#3cc2be","#5addd9","#6afbf4"], # 5 BG + ["#0c1f24","#00373e","#155057","#336970","#28878f","#46a2aa","#37c1c8","#55dde4","#92f5fb"], # 7.5 BG + ["#0c1f26","#23343b","#154f5c","#336975","#278697","#46a1b1","#34c0d2","#52dced","#b5effd"], # 10 BG + ["#0c1f27","#23343c","#154f60","#356879","#2b859d","#49a0b8","#3bbedb","#55dbf7","#d5e8f8"], # 2.5 B + ["#0c1f29","#023649","#1a4e64","#076988","#3584a2","#19a1ca","#49bce4","#88d4f5","#d8e7f9"], # 5 B + ["#0e1e2a","#0a354c","#224d66","#21688b","#0284b3","#3a9fce","#58baea","#90d2f9","#dbe6fa"], # 7.5 B + ["#101e2c","#13344d","#294b68","#2f668d","#2d82b6","#1f9edf","#43b9fe","#9bd0fd","#dee5fa"], # 10 B + ["#021e38","#1c334f","#1d4b77","#23649e","#277fc7","#159bf3","#7eb3f0","#b8cbee","#e2e4fa"], # 2.5 PB + ["#0d1c38","#14315d","#1a4885","#2661ac","#2e7cd6","#4f96f4","#8eb0f1","#bec9ef","#e4e4fa"], # 5 PB + ["#1c0560","#2c089c","#3b0ddf","#3f45f7","#566ff1","#7d8ef2","#a2abf2","#c7c7ee","#e8e3fa"], # 7.5 PB + ["#290652","#45018a","#5f0fbf","#7d16fe","#8261f3","#9784fb","#b0a4fe","#ccc3fe","#eae2f9"], # 10 PB + ["#2e0945","#510079","#7103ab","#911ddb","#a945fd","#b577fd","#c1a0f7","#d5c1fa","#ece1f9"], # 2.5 P + ["#340541","#54086b","#79079a","#9d18c8","#c226f9","#ca6bfc","#d397fc","#dcbff5","#ede1f8"], # 5 P + ["#37023f","#55115e","#7e0e8a","#a519b3","#d11ee0","#f045ff","#ee8af6","#f2b6f7","#f1e0f5"], # 7.5 P + ["#340a36","#5a0c59","#7f177a","#a9229e","#d822c6","#f749e2","#fa86e8","#fab4ee","#f2e0f3"], # 10 P + ["#360833","#600450","#880c6d","#b3128e","#e401b2","#fa4fc6","#fa8ad2","#f4b9de","#f4e0f2"], # 2.5 RP + ["#39062f","#5b1344","#8d0060","#b32078","#e71994","#fb56aa","#f68fbd","#f9b8d5","#f5dff0"], # 5 RP + ["#3b052b","#5e113e","#881951","#b81a69","#ec0c82","#f56099","#fa8eb3","#fcb7cf","#f6dfee"], # 7.5 RP + ["#3d0427","#600f38","#8c1645","#bd145a","#e72a6f","#f95f88","#fe8ea7","#ffb7c8","#f7dfed"], # 10 RP + ["#3f0222","#620d31","#8d153a","#bf124b","#e92a5e","#fb5f79","#f295a1","#efbdc8","#f6dfeb"], # 2.5 R +] + +# neutral values from 0 to 10. Not sure these are completely +# accurate; my method was a bit of a guesstimate. +munsell_neutrals = [ + '#000000', '#1d1d1d', '#323232', '#494949', '#626262', '#7c7c7c', + '#969696', '#b1b1b1', '#cbcbcb', '#e7e7e7', '#ffffff' +] + + +def _hex_color(hue, value): + # hue ranges from 0 (5R) to 39 (2.5R). value ranges from 1 to 9 + return munsell_colors[hue][value-1] + +def rand(n): + return int(math.floor(random.random() * n)) + + +class ColorPicker(hippo.CanvasBox, hippo.CanvasItem): + __gsignals__ = { + 'color': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + } + + def __init__(self, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + self.props.orientation = hippo.ORIENTATION_HORIZONTAL + + # 5YR7: #f69955 + # 5BP3: #1a4885 + self._fg_hue = 4 + self._fg_value = 7 + self._bg_hue = 28 + self._bg_value = 3 + + self._fg_hex = _hex_color(self._fg_hue, self._fg_value) + self._bg_hex = _hex_color(self._bg_hue, self._bg_value) + + self._pie_hues = 10 + self._slider_values = 9 + + self._xo = CanvasIcon(scale=units.XLARGE_ICON_SCALE, + icon_name='theme:stock-buddy', + stroke_color=color.HTMLColor(self._fg_hex), + fill_color=color.HTMLColor(self._bg_hex)) + self._set_random_colors() + self._emit_color() + self._xo.connect('activated', self._xo_activated_cb) + self.append(self._xo) + + def _xo_activated_cb(self, item): + self._set_random_colors() + self._emit_color() + + def _emit_color(self): + xo_color = XoColor('%s,%s' % (self._xo.props.stroke_color.get_html(), + self._xo.props.fill_color.get_html())) + self.emit('color', xo_color) + + def _update_xo_hex(self, fg=None, bg=None): + """set the colors of the XO man""" + if fg: + self._xo.props.stroke_color = color.HTMLColor(fg) + if bg: + self._xo.props.fill_color = color.HTMLColor(bg) + + def _update_fg_hue(self, pie_fg_hue): + """change foreground (fill) hue""" + self._fg_hue = pie_fg_hue * (40 / self._pie_hues) + self._fg_hex = _hex_color(self._fg_hue, self._fg_value) + + self._update_xo_hex(fg=self._fg_hex) + + # set value slider + #for i in range(1, 10): + # setFill("fgv" + i, _hex_color(self._fg_hue, i)) + + # rotate selection dingus + #svgDocument.getElementById("fgHueSelect").setAttribute( + # "transform", + # "rotate(" + (360/self._pie_hues) * pie_fg_hue + ")" + #) + + def _update_bg_hue(self, pie_bg_hue): + """change background (stroke) hue""" + self._bg_hue = pie_bg_hue * (40 / self._pie_hues) + self._bg_hex = _hex_color(self._bg_hue, self._bg_value) + + self._update_xo_hex(bg=self._bg_hex) + + # set value slider + #for i in range(1, self._slider_values + 1): + # setFill("bgv" + i, _hex_color(self._bg_hue, i)) + + # rotate selection dingus + #svgDocument.getElementById("bgHueSelect").setAttribute( + # "transform", + # "rotate(" + (360/self._pie_hues) * pie_bg_hue + ")" + #) + + def _update_fg_value(self, slider_fg_value): + self._fg_value = slider_fg_value + self._fg_hex = _hex_color(self._fg_hue, self._fg_value) + + self._update_xo_hex(fg=self._fg_hex) + + # set hue pie + #for i in range(0, self._pie_hues): + # cur_hue = i * (40 / self._pie_hues) + # setFill("fgh" + i, _hex_color(cur_hue, self._fg_value)) + + # move selection dingus + #svgDocument.getElementById("fgValueSelect").setAttribute( + # "transform", + # "translate(0 -" + 22 * slider_fg_value + ")" + #) + + def _update_bg_value(self, slider_bg_value): + self._bg_value = slider_bg_value + self._bg_hex = _hex_color(self._bg_hue, self._bg_value) + + self._update_xo_hex(bg=self._bg_hex) + + # set hue pie + #for i in range(0, self._pie_hues): + # cur_hue = i * (40 / self._pie_hues) + # setFill("bgh" + i, _hex_color(cur_hue, self._bg_value)) + + # move selection dingus + #svgDocument.getElementById("bgValueSelect").setAttribute( + # "transform", + # "translate(0 -" + 22 * slider_bg_value + ")" + #) + + def _set_random_colors(self): + self._update_fg_hue(rand(self._pie_hues)) + self._update_fg_value(rand(self._slider_values)+1) + self._update_bg_hue(rand(self._pie_hues)) + self._update_bg_value(rand(self._slider_values)+1) + diff --git a/shell/intro/glive.py b/shell/intro/glive.py new file mode 100644 index 00000000..f02b261d --- /dev/null +++ b/shell/intro/glive.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python +# -*- Mode: Python -*- +# vi:si:et:sw=4:sts=4:ts=4 + +import gtk +import pygtk +pygtk.require('2.0') +import sys + +import pygst +pygst.require('0.10') +import gst +import gst.interfaces + +import gobject +gobject.threads_init() + +class Glive(gobject.GObject): + __gsignals__ = { + 'new-picture': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + 'sink': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])) + } + + def __init__(self, parent, width, height): + self._parent = parent + + #check out the halfpipe, d00d. + self.pipeline = gst.Pipeline() + + self.v4l2src = gst.element_factory_make("v4l2src", "v4l2src") + self.t = gst.element_factory_make("tee", "tee") + self.t_src_pad = self.t.get_request_pad( "src%d" ) + self.vscale = gst.element_factory_make("videoscale", "videoscale") + self.ximagesink = gst.element_factory_make("ximagesink", "ximagesink") + + self.pipeline.add(self.v4l2src) + self.pipeline.add(self.t) + self.pipeline.add(self.vscale) + self.pipeline.add(self.ximagesink) + + self.v4l2src.link(self.t) + + videoscale_structure = gst.Structure("video/x-raw-rgb") + videoscale_structure['width'] = width + videoscale_structure['height'] = height + videoscale_structure['bpp'] = 16 + videoscale_structure['depth'] = 16 + videoscale_caps = gst.Caps(videoscale_structure) + self.t_src_pad.link(self.vscale.get_pad("sink")) + self.vscale.link(self.ximagesink, videoscale_caps) + #self.vscale.link(self.ximagesink) + + self.queue = gst.element_factory_make("queue", "queue") + self.queue.set_property("leaky", True) + self.queue.set_property("max-size-buffers", 1) + self.qsrc = self.queue.get_pad( "src" ) + self.qsink = self.queue.get_pad("sink") + self.ffmpeg = gst.element_factory_make("ffmpegcolorspace", "ffmpegcolorspace") + self.jpgenc = gst.element_factory_make("jpegenc", "jpegenc") + self.filesink = gst.element_factory_make("fakesink", "fakesink") + self.filesink.connect( "handoff", self.copyframe ) + self.filesink.set_property("signal-handoffs", True) + self.pipeline.add(self.queue, self.ffmpeg, self.jpgenc, self.filesink) + + #only link at snapshot time + #self.t.link(self.queue) + self.queue.link(self.ffmpeg) + self.ffmpeg.link(self.jpgenc) + self.jpgenc.link(self.filesink) + self.exposureOpen = False + + self._bus = self.pipeline.get_bus() + self._CONNECT_SYNC = -1 + self._CONNECT_MSG = -1 + self.doPostBusStuff() + + def copyframe(self, fsink, buffer, pad, user_data=None): + #for some reason, we get two back to back buffers, even though we + #ask for only one. + if (self.exposureOpen): + self.exposureOpen = False + piccy = gtk.gdk.pixbuf_loader_new_with_mime_type("image/jpeg") + piccy.write( buffer ) + piccy.close() + pixbuf = piccy.get_pixbuf() + del piccy + + self.t.unlink(self.queue) + self.queue.set_property("leaky", True) + + gobject.idle_add(self.loadPic, pixbuf) + + def loadPic( self, pixbuf ): + self.emit('new-picture', pixbuf) + + def takeSnapshot( self ): + if (self.exposureOpen): + return + else: + self.exposureOpen = True + self.t.link(self.queue) + + def doPostBusStuff(self): + self._bus.enable_sync_message_emission() + self._bus.add_signal_watch() + self._CONNECT_SYNC = self._bus.connect('sync-message::element', self.on_sync_message) + self._CONNECT_MSG = self._bus.connect('message', self.on_message) + + def on_sync_message(self, bus, message): + if message.structure is None: + return + if message.structure.get_name() == 'prepare-xwindow-id': + self.emit('sink', message.src) + message.src.set_property('force-aspect-ratio', True) + + def on_message(self, bus, message): + t = message.type + if (t == gst.MESSAGE_ERROR): + err, debug = message.parse_error() + if (self.on_eos): + self.on_eos() + self._playing = False + elif (t == gst.MESSAGE_EOS): + if (self.on_eos): + self.on_eos() + self._playing = False + + def on_eos( self ): + pass + + def stop(self): + self.pipeline.set_state(gst.STATE_NULL) + + def play(self): + self.pipeline.set_state(gst.STATE_PLAYING) + + def pause(self): + self.pipeline.set_state(gst.STATE_PAUSED) + + +class LiveVideoSlot(gtk.EventBox): + __gsignals__ = { + 'pixbuf': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + } + + def __init__(self, width, height): + gtk.EventBox.__init__(self) + + self.imagesink = None + self.unset_flags(gtk.DOUBLE_BUFFERED) + self.connect('focus-in-event', self.focus_in) + self.connect('focus-out-event', self.focus_out) + self.connect("button-press-event", self.button_press) + + self.playa = Glive(self, width, height) + self.playa.connect('new-picture', self._new_picture_cb) + self.playa.connect('sink', self._new_sink_cb) + + def _new_picture_cb(self, playa, pixbuf): + self.emit('pixbuf', pixbuf) + + def _new_sink_sb(self, playa, sink): + if (self.imagesink != None): + assert self.window.xid + self.imagesink = None + del self.imagesink + self.imagesink = sink + self.imagesink.set_xwindow_id(self.window.xid) + + def focus_in(self, widget, event, args=None): + self.play() + + def focus_out(self, widget, event, args=None): + self.stop() + + def play( self ): + self.playa.play() + + def pause( self ): + self.playa.pause() + + def stop( self ): + self.playa.stop() + + def takeSnapshot( self ): + self.playa.takeSnapshot() diff --git a/shell/intro/intro.py b/shell/intro/intro.py new file mode 100644 index 00000000..f6e9fdff --- /dev/null +++ b/shell/intro/intro.py @@ -0,0 +1,238 @@ +# Copyright (C) 2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk, gobject +import hippo +import logging +from gettext import gettext as _ + +import os +from ConfigParser import ConfigParser + +from sugar import env + +from sugar.graphics import entry +from sugar.graphics import units +from sugar.graphics import font +from sugar.graphics import color +from sugar.graphics import iconbutton + +import colorpicker + +_VIDEO_WIDTH = 320 +_VIDEO_HEIGHT = 240 + + +class IntroFallbackVideo(gtk.EventBox): + __gtype_name__ = "IntroFallbackVideo" + + __gsignals__ = { + 'pixbuf': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([gobject.TYPE_PYOBJECT])), + } + + def __init__(self, **kwargs): + gtk.EventBox.__init__(self, **kwargs) + self._image = gtk.Image() + self._image.set_from_stock(gtk.STOCK_OPEN, -1) + self.add(self._image) + self.connect('button-press-event', self._button_press_cb) + + def _button_press_cb(self, widget, event): + filt = gtk.FileFilter() + filt.add_pixbuf_formats() + chooser = gtk.FileChooserDialog(_("Pick a buddy picture"), \ + buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) + chooser.set_filter(filt) + resp = chooser.run() + if resp == gtk.RESPONSE_ACCEPT: + fname = chooser.get_filename() + pixbuf = gtk.gdk.pixbuf_new_from_file(fname) + (w, h) = self.get_size_request() + img_pixbuf = pixbuf.scale_simple(w, h, gtk.gdk.INTERP_BILINEAR) + self._image.set_from_pixbuf(img_pixbuf) + self.emit('pixbuf', pixbuf) + chooser.hide() + chooser.destroy() + return True + +class VideoBox(hippo.CanvasBox, hippo.CanvasItem): + __gtype_name__ = "SugarVideoBox" + + def __init__(self, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + self.props.orientation = hippo.ORIENTATION_HORIZONTAL + self._pixbuf = None + + self._label = hippo.CanvasText(text=_("My Picture:"), + xalign=hippo.ALIGNMENT_START, + padding_right=units.grid_to_pixels(0.5)) + self._label.props.color = color.LABEL_TEXT.get_int() + self._label.props.font_desc = font.DEFAULT.get_pango_desc() + self.append(self._label) + + try: + import glive + self._video = glive.LiveVideoSlot(_VIDEO_WIDTH, _VIDEO_HEIGHT) + except ImportError: + self._video = IntroFallbackVideo() + + self._video.set_size_request(_VIDEO_WIDTH, _VIDEO_HEIGHT) + self._video.connect('pixbuf', self._new_pixbuf_cb) + + self._video_widget = hippo.CanvasWidget() + self._video_widget.props.widget = self._video + self.append(self._video_widget) + + def _new_pixbuf_cb(self, widget, pixbuf): + if self._pixbuf: + del self._pixbuf + self._pixbuf = pixbuf + + def get_pixbuf(self): + return self._pixbuf + +class EntryBox(hippo.CanvasBox, hippo.CanvasItem): + __gtype_name__ = "SugarEntryBox" + + def __init__(self, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + self.props.orientation = hippo.ORIENTATION_HORIZONTAL + + self._label = hippo.CanvasText(text=_("My Name:"), + xalign=hippo.ALIGNMENT_START, + padding_right=units.grid_to_pixels(0.5)) + self._label.props.color = color.LABEL_TEXT.get_int() + self._label.props.font_desc = font.DEFAULT.get_pango_desc() + self.append(self._label) + + self._entry = entry.Entry() + self.append(self._entry) + + def get_text(self): + return self._entry.props.text + + +class ColorBox(hippo.CanvasBox, hippo.CanvasItem): + __gtype_name__ = "SugarColorBox" + + def __init__(self, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + self.props.orientation = hippo.ORIENTATION_HORIZONTAL + self._color = None + + self._label = hippo.CanvasText(text=_("My Color:"), + xalign=hippo.ALIGNMENT_START, + padding_right=units.grid_to_pixels(0.5)) + self._label.props.color = color.LABEL_TEXT.get_int() + self._label.props.font_desc = font.DEFAULT.get_pango_desc() + self.append(self._label) + + self._cp = colorpicker.ColorPicker() + self._cp.connect('color', self._new_color_cb) + self.append(self._cp) + + def _new_color_cb(self, widget, color): + self._color = color + + def get_color(self): + return self._color + +class IntroBox(hippo.CanvasBox, hippo.CanvasItem): + __gtype_name__ = 'SugarIntroBox' + + def __init__(self, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + self._pixbuf = None + + self._video_box = VideoBox(xalign=hippo.ALIGNMENT_CENTER, + yalign=hippo.ALIGNMENT_START, + padding_bottom=units.grid_to_pixels(1)) + self.append(self._video_box) + + self._entry_box = EntryBox(xalign=hippo.ALIGNMENT_CENTER, + padding_bottom=units.grid_to_pixels(1)) + self.append(self._entry_box) + + self._color_box = ColorBox(xalign=hippo.ALIGNMENT_CENTER, + padding_bottom=units.grid_to_pixels(1)) + self.append(self._color_box) + + self._ok = iconbutton.IconButton(icon_name="theme:stock-forward", + padding_bottom=units.grid_to_pixels(0.5)) + self._ok.connect('activated', self._ok_activated) + self.append(self._ok) + + def _ok_activated(self, item): + pixbuf = self._video_box.get_pixbuf() + name = self._entry_box.get_text() + color = self._color_box.get_color() + + if not pixbuf or not name or not color: + return + + self._create_profile(pixbuf, name, color) + gtk.main_quit() + + def _create_profile(self, pixbuf, name, color): + # Save the buddy icon + icon_path = os.path.join(env.get_profile_path(), "buddy-icon.jpg") + scaled = pixbuf.scale_simple(200, 200, gtk.gdk.INTERP_BILINEAR) + pixbuf.save(icon_path, "jpeg", {"quality":"85"}) + + cp = ConfigParser() + section = 'Buddy' + cp.add_section(section) + cp.set(section, 'NickName', name) + cp.set(section, 'Color', color.to_string()) + + config_path = os.path.join(env.get_profile_path(), 'config') + f = open(config_path, 'w') + cp.write(f) + f.close() + + # Generate keypair + import commands + keypath = os.path.join(env.get_profile_path(), "owner.key") + cmd = "ssh-keygen -q -t dsa -f %s -C '' -N ''" % keypath + (s, o) = commands.getstatusoutput(cmd) + if s != 0: + logging.error("Could not generate key pair: %d" % s) + + +class IntroWindow(gtk.Window): + def __init__(self): + gtk.Window.__init__(self) + self.set_default_size(gtk.gdk.screen_width(), + gtk.gdk.screen_height()) + self.realize() + self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DESKTOP) + + self._canvas = hippo.Canvas() + self._intro_box = IntroBox(background_color=0x000000ff, + yalign=hippo.ALIGNMENT_START, + padding_top=units.grid_to_pixels(2), + padding_left=units.grid_to_pixels(3), + padding_right=units.grid_to_pixels(3)) + self._canvas.set_root(self._intro_box) + self.add(self._canvas) + self._canvas.show() + + +if __name__ == "__main__": + w = IntroWindow() + w.show_all() + w.connect('destroy', gtk.main_quit) + gtk.main() diff --git a/shell/sugar-shell b/shell/sugar-shell index 54c1705f..92cefc32 100755 --- a/shell/sugar-shell +++ b/shell/sugar-shell @@ -38,15 +38,17 @@ logger.start('shell') if len(sys.argv) == 1: sys.path.insert(0, os.path.join(env.get_data_dir(), 'shell')) -from view.FirstTimeDialog import FirstTimeDialog from view.Shell import Shell from model.ShellModel import ShellModel from shellservice import ShellService +from intro import intro -name = profile.get_nick_name() -if not name or not len(name): - dialog = FirstTimeDialog() - dialog.run() +# Do initial setup if needed +key = profile.get_pubkey() +if not key or not len(key): + win = intro.IntroWindow() + win.show_all() + gtk.main() profile.update() # Save our DBus Session Bus address somewhere it can be found diff --git a/sugar/graphics/color.py b/sugar/graphics/color.py index 32d248cc..864e8e39 100644 --- a/sugar/graphics/color.py +++ b/sugar/graphics/color.py @@ -62,7 +62,7 @@ class RGBColor(object): int(self._b * 65535)) def get_html(self): - return '#%x%x%x' % (self._r * 255, self._g * 255, self._b * 255) + return '#%02x%02x%02x' % (self._r * 255, self._g * 255, self._b * 255) class HTMLColor(RGBColor): def __init__(self, html_color): diff --git a/sugar/profile.py b/sugar/profile.py index cb1309b2..f34e390b 100644 --- a/sugar/profile.py +++ b/sugar/profile.py @@ -15,9 +15,11 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import os +import logging from ConfigParser import ConfigParser from sugar import env +from sugar import util from sugar.graphics.xocolor import XoColor class _Profile(object): @@ -25,6 +27,7 @@ class _Profile(object): self.name = None self.color = None self.pubkey = None + self.privkey_hash = None self._load() def update(self): @@ -41,11 +44,60 @@ class _Profile(object): if cp.has_option('Buddy', 'Color'): self.color = XoColor(cp.get('Buddy', 'Color')) - if cp.has_option('Buddy', 'PublicKey'): - self.pubkey = cp.get('Buddy', 'PublicKey') - del cp + self._load_pubkey() + self._hash_private_key() + + def _load_pubkey(self): + self.pubkey = None + + key_path = os.path.join(env.get_profile_path(), 'owner.key.pub') + try: + f = open(key_path, "r") + lines = f.readlines() + f.close() + except IOError, e: + logging.error("Error reading public key: %s" % e) + return + + magic = "ssh-dss " + for l in lines: + l = l.strip() + if not l.startswith(magic): + continue + self.pubkey = l[len(magic):] + break + if not self.pubkey: + logging.error("Error parsing public key.") + + def _hash_private_key(self): + self.privkey_hash = None + + key_path = os.path.join(env.get_profile_path(), 'owner.key') + try: + f = open(key_path, "r") + lines = f.readlines() + f.close() + except IOError, e: + logging.error("Error reading private key: %s" % e) + return + + key = "" + for l in lines: + l = l.strip() + if l.startswith("-----BEGIN DSA PRIVATE KEY-----"): + continue + if l.startswith("-----END DSA PRIVATE KEY-----"): + continue + key += l + if not len(key): + logging.error("Error parsing public key.") + + # hash it + key_hash = util._sha_data(key) + self.privkey_hash = util.printable_hash(key_hash) + def get_nick_name(): return _profile.name @@ -55,6 +107,9 @@ def get_color(): def get_pubkey(): return _profile.pubkey +def get_private_key_hash(): + return _profile.privkey_hash + def update(): _profile.update()