diff --git a/sugar/Makefile.am b/sugar/Makefile.am new file mode 100644 index 00000000..7293e3c3 --- /dev/null +++ b/sugar/Makefile.am @@ -0,0 +1,10 @@ +SUBDIRS = chat browser shell + +bin_SCRIPTS = sugar + +sugardir = $(pythondir)/sugar +sugar_PYTHON = \ + __init__.py \ + __installed__.py + +EXTRA_DIST = sugar diff --git a/sugar/__init__.py b/sugar/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sugar/__installed__.py.in b/sugar/__installed__.py.in new file mode 100644 index 00000000..717669d8 --- /dev/null +++ b/sugar/__installed__.py.in @@ -0,0 +1,2 @@ +def internal_get_data_file(filename): + return "@prefix@/share/sugar" diff --git a/sugar/__uninstalled__.py b/sugar/__uninstalled__.py new file mode 100644 index 00000000..4ffafc83 --- /dev/null +++ b/sugar/__uninstalled__.py @@ -0,0 +1,13 @@ +import os + +data_dirs = [ 'sugar/browser', 'sugar/chat' ] + +def internal_get_data_file(filename): + basedir = os.path.dirname(os.path.dirname(__file__)) + + for data_dir in data_dirs: + path = os.path.abspath(os.path.join(basedir, data_dir, filename)) + if os.path.isfile(path): + return path + + return None diff --git a/sugar/browser/Makefile.am b/sugar/browser/Makefile.am new file mode 100644 index 00000000..d7e4b354 --- /dev/null +++ b/sugar/browser/Makefile.am @@ -0,0 +1,24 @@ +sugardir = $(pythondir)/sugar/browser +sugar_PYTHON = browser.py + +icondir = $(pkgdatadir) +icon_DATA = \ + fold.png \ + unfold.png + +# Dbus service file +servicedir = $(datadir)/dbus-1/services +service_in_files = com.redhat.Sugar.Browser.service.in +service_DATA = $(service_in_files:.service.in=.service) + +# Rule to make the service file with bindir expanded +$(service_DATA): $(service_in_files) Makefile + @sed -e "s|\@bindir\@|$(bindir)|" $< > $@ + +EXTRA_DIST = \ + $(service_in_files) \ + $(service_DATA) \ + $(icon_DATA) + +DISTCLEANFILES = \ + $(service_DATA) diff --git a/sugar/browser/browser.py b/sugar/browser/browser.py new file mode 100755 index 00000000..633fa044 --- /dev/null +++ b/sugar/browser/browser.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python + +import dbus +import dbus.service +import dbus.glib + +import pygtk +pygtk.require('2.0') +import gtk + +import geckoembed + +from sugar.shell import activity +import sugar.env + +class AddressToolbar(gtk.Toolbar): + def __init__(self): + gtk.Toolbar.__init__(self) + + address_item = AddressItem(self.__open_address_cb) + self.insert(address_item, 0) + address_item.show() + + def __open_address_cb(self, address): + BrowserShell.get_instance().open_browser(address) + +class AddressItem(gtk.ToolItem): + def __init__(self, callback): + gtk.ToolItem.__init__(self) + + address_entry = AddressEntry(callback) + self.add(address_entry) + address_entry.show() + +class AddressEntry(gtk.HBox): + def __init__(self, callback): + gtk.HBox.__init__(self) + + self.callback = callback + self.folded = True + + label = gtk.Label("Open") + self.pack_start(label, False) + label.show() + + self.button = gtk.Button() + self.button.set_relief(gtk.RELIEF_NONE) + self.button.connect("clicked", self.__button_clicked_cb) + self.pack_start(self.button, False) + self.button.show() + + self.entry = gtk.Entry() + self.entry.connect("activate", self.__activate_cb) + self.pack_start(self.entry, False) + self.entry.show() + + self._update_folded_state() + + def _update_folded_state(self): + if self.folded: + image = gtk.Image() + image.set_from_file(sugar.env.get_data_file('unfold.png')) + self.button.set_image(image) + image.show() + + self.entry.hide() + else: + image = gtk.Image() + image.set_from_file(sugar.env.get_data_file('fold.png')) + self.button.set_image(image) + image.show() + + self.entry.show() + self.entry.grab_focus() + + def get_folded(self): + return self.folded + + def set_folded(self, folded): + self.folded = not self.folded + self._update_folded_state() + + def __button_clicked_cb(self, button): + self.set_folded(not self.get_folded()) + + def __activate_cb(self, entry): + self.callback(entry.get_text()) + self.set_folded(True) + +class NavigationToolbar(gtk.Toolbar): + def __init__(self, embed): + gtk.Toolbar.__init__(self) + self.embed = embed + + self.set_style(gtk.TOOLBAR_ICONS) + + self.back = gtk.ToolButton(gtk.STOCK_GO_BACK) + self.back.connect("clicked", self.__go_back_cb) + self.insert(self.back, -1) + self.back.show() + + self.forward = gtk.ToolButton(gtk.STOCK_GO_FORWARD) + self.forward.connect("clicked", self.__go_forward_cb) + self.insert(self.forward, -1) + self.forward.show() + + self.reload = gtk.ToolButton(gtk.STOCK_REFRESH) + self.reload.connect("clicked", self.__reload_cb) + self.insert(self.reload, -1) + self.reload.show() + + separator = gtk.SeparatorToolItem() + self.insert(separator, -1) + separator.show() + + share = gtk.ToolButton("Share") + share.connect("clicked", self.__share_cb) + self.insert(share, -1) + share.show() + + separator = gtk.SeparatorToolItem() + self.insert(separator, -1) + separator.show() + + address_item = AddressItem(self.__open_address_cb) + self.insert(address_item, -1) + address_item.show() + + self._update_sensitivity() + + self.embed.connect("location", self.__location_changed) + + def _update_sensitivity(self): + self.back.set_sensitive(self.embed.can_go_back()) + self.forward.set_sensitive(self.embed.can_go_forward()) + + def __go_back_cb(self, button): + self.embed.go_back() + + def __go_forward_cb(self, button): + self.embed.go_forward() + + def __reload_cb(self, button): + self.embed.reload() + + def __share_cb(self, button): + pass + + def __location_changed(self, embed): + self._update_sensitivity() + + def __open_address_cb(self, address): + self.embed.load_address(address) + +class BrowserActivity(activity.Activity): + def __init__(self, uri): + activity.Activity.__init__(self) + self.uri = uri + + def activity_on_connected_to_shell(self): + self.activity_set_ellipsize_tab(True) + self.activity_set_can_close(True) + self.activity_set_tab_text("Web Page") + self.activity_set_tab_icon_name("web-browser") + self.activity_show_icon(True) + + vbox = gtk.VBox() + + self.embed = geckoembed.Embed() + self.embed.connect("title", self.__title_cb) + vbox.pack_start(self.embed) + + self.embed.show() + self.embed.load_address(self.uri) + + nav_toolbar = NavigationToolbar(self.embed) + vbox.pack_start(nav_toolbar, False) + nav_toolbar.show() + + plug = self.activity_get_gtk_plug() + plug.add(vbox) + plug.show() + + vbox.show() + + def get_embed(self): + return self.embed + + def __title_cb(self, embed): + self.activity_set_tab_text(embed.get_title()) + + def activity_on_close_from_user(self): + self.activity_shutdown() + +class WebActivity(activity.Activity): + def __init__(self): + activity.Activity.__init__(self) + + def activity_on_connected_to_shell(self): + self.activity_set_tab_text("Web Browser") + self.activity_set_tab_icon_name("web-browser") + self.activity_show_icon(True) + + vbox = gtk.VBox() + + self.embed = geckoembed.Embed() + self.embed.connect("open-address", self.__open_address); + vbox.pack_start(self.embed) + self.embed.show() + + address_toolbar = AddressToolbar() + vbox.pack_start(address_toolbar, False) + address_toolbar.show() + + plug = self.activity_get_gtk_plug() + plug.add(vbox) + plug.show() + + vbox.show() + + self.embed.load_address("http://www.google.com") + + def __open_address(self, embed, uri, data=None): + if uri.startswith("http://www.google.com"): + return False + else: + BrowserShell.get_instance().open_browser(uri) + return True + + def activity_on_disconnected_from_shell(self): + gtk.main_quit() + gc.collect() + +class BrowserShell(dbus.service.Object): + instance = None + + def get_instance(): + if not BrowserShell.instance: + BrowserShell.instance = BrowserShell() + return BrowserShell.instance + + get_instance = staticmethod(get_instance) + + def __init__(self): + session_bus = dbus.SessionBus() + bus_name = dbus.service.BusName('com.redhat.Sugar.Browser', bus=session_bus) + object_path = '/com/redhat/Sugar/Browser' + + dbus.service.Object.__init__(self, bus_name, object_path) + + self.__browsers = [] + + def open_web_activity(self): + web_activity = WebActivity() + web_activity.activity_connect_to_shell() + + @dbus.service.method('com.redhat.Sugar.BrowserShell') + def get_links(self): + links = [] + for browser in self.__browsers: + embed = browser.get_embed() + link = {} + link['title'] = embed.get_title() + link['address'] = embed.get_address() + links.append(link) + return links + + @dbus.service.method('com.redhat.Sugar.BrowserShell') + def open_browser(self, uri): + browser = BrowserActivity(uri) + self.__browsers.append(browser) + browser.activity_connect_to_shell() + +def main(): + BrowserShell.get_instance().open_web_activity() + + try: + gtk.main() + except KeyboardInterrupt: + pass + +if __name__=="__main__": + main() diff --git a/sugar/browser/com.redhat.Sugar.Browser.service.in b/sugar/browser/com.redhat.Sugar.Browser.service.in new file mode 100644 index 00000000..654095a6 --- /dev/null +++ b/sugar/browser/com.redhat.Sugar.Browser.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=com.redhat.Sugar.Browser +Exec=@bindir@/sugar browser diff --git a/sugar/browser/fold.png b/sugar/browser/fold.png new file mode 100644 index 00000000..cd4169ba Binary files /dev/null and b/sugar/browser/fold.png differ diff --git a/sugar/browser/unfold.png b/sugar/browser/unfold.png new file mode 100644 index 00000000..f3f82fae Binary files /dev/null and b/sugar/browser/unfold.png differ diff --git a/sugar/chat/BuddyList.py b/sugar/chat/BuddyList.py new file mode 100644 index 00000000..d35fa847 --- /dev/null +++ b/sugar/chat/BuddyList.py @@ -0,0 +1,121 @@ +# -*- tab-width: 4; indent-tabs-mode: t -*- + +import presence +import avahi + +ACTION_BUDDY_ADDED = "added" +ACTION_BUDDY_REMOVED = "removed" + + +class Buddy(object): + def __init__(self, nick, realname, servicename, host, address, port, key=None): + self._nick = nick + self._realname = realname + self._servicename = servicename + self._key = key + self._host = host + self._address = str(address) + self._port = int(port) + self._chat = None + + def set_chat(self, chat): + self._chat = chat + + def chat(self): + return self._chat + + def nick(self): + return self._nick + + def realname(self): + return self._realname + + def servicename(self): + return self._servicename + + def host(self): + return self._host + + def address(self): + return self._address + + def port(self): + return self._port + + def key(self): + return self._key + +class BuddyList(object): + """ Manage a list of buddies """ + + def __init__(self, servicename): + self._listeners = [] + self._buddies = {} + self._servicename = servicename + self._pdiscovery = presence.PresenceDiscovery() + self._pdiscovery.add_service_listener(self._on_service_change) + + def start(self): + self._pdiscovery.start() + + def add_buddy_listener(self, listener): + self._listeners.append(listener) + + def _add_buddy(self, host, address, port, servicename, data): + # Ignore ourselves + if servicename == self._servicename: + return + + if len(data) > 0 and 'name' in data.keys(): + buddy = self._find_buddy_by_service_name(servicename) + if not buddy: + buddy = Buddy(data['name'], data['realname'], servicename, host, address, port) + self._buddies[data['name']] = buddy + self._notify_listeners(ACTION_BUDDY_ADDED, buddy) + + def _remove_buddy(self, buddy): + nick = buddy.nick() + self._notify_listeners(ACTION_BUDDY_REMOVED, buddy) + del self._buddies[nick] + + def _find_buddy_by_service_name(self, servicename): + for buddy in self._buddies.values(): + if buddy.servicename() == servicename: + return buddy + return None + + def find_buddy_by_address(self, address): + for buddy_name in self._buddies.keys(): + buddy = self._buddies[buddy_name] + if buddy.address() == address: + return buddy + return None + + def _notify_listeners(self, action, buddy): + for listener in self._listeners: + listener(action, buddy) + + def _on_service_change(self, action, interface, protocol, name, stype, domain, flags): + if stype != presence.OLPC_CHAT_SERVICE: + return + if action == presence.ACTION_SERVICE_NEW: + self._pdiscovery.resolve_service(interface, protocol, name, stype, domain, self._on_service_resolved) + elif action == presence.ACTION_SERVICE_REMOVED: + buddy = self._find_buddy_by_service_name(name) + if buddy: + self._remove_buddy(buddy) + + def _pair_to_dict(self, l): + res = {} + for el in l: + tmp = el.split('=', 1) + if len(tmp) > 1: + res[tmp[0]] = tmp[1] + else: + res[tmp[0]] = '' + return res + + def _on_service_resolved(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags): + data = self._pair_to_dict(avahi.txt_array_to_string_array(txt)) + self._add_buddy(host, address, port, name, data) + diff --git a/sugar/chat/Makefile.am b/sugar/chat/Makefile.am new file mode 100644 index 00000000..4ddf6e96 --- /dev/null +++ b/sugar/chat/Makefile.am @@ -0,0 +1,11 @@ +sugardir = $(pythondir)/sugar/chat +sugar_PYTHON = \ + chat.py \ + richtext.py + +icondir = $(pkgdatadir) +icon_DATA = \ + bubble.png \ + bubbleOutline.png + +EXTRA_DIST = $(icon_DATA) diff --git a/sugar/chat/SVGdraw.py b/sugar/chat/SVGdraw.py new file mode 100644 index 00000000..abcda112 --- /dev/null +++ b/sugar/chat/SVGdraw.py @@ -0,0 +1,1069 @@ +#!/usr/bin/env python +##Copyright (c) 2002, Fedor Baart & Hans de Wit (Stichting Farmaceutische Kengetallen) +##All rights reserved. +## +##Redistribution and use in source and binary forms, with or without modification, +##are permitted provided that the following conditions are met: +## +##Redistributions of source code must retain the above copyright notice, this +##list of conditions and the following disclaimer. +## +##Redistributions in binary form must reproduce the above copyright notice, +##this list of conditions and the following disclaimer in the documentation and/or +##other materials provided with the distribution. +## +##Neither the name of the Stichting Farmaceutische Kengetallen nor the names of +##its contributors may be used to endorse or promote products derived from this +##software without specific prior written permission. +## +##THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +##AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +##IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +##DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +##FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +##DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +##SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +##CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +##OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +##OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +##Thanks to Gerald Rosennfellner for his help and useful comments. + +__doc__="""Use SVGdraw to generate your SVGdrawings. + +SVGdraw uses an object model drawing and a method toXML to create SVG graphics +by using easy to use classes and methods usualy you start by creating a drawing eg + + d=drawing() + #then you create a SVG root element + s=svg() + #then you add some elements eg a circle and add it to the svg root element + c=circle() + #you can supply attributes by using named arguments. + c=circle(fill='red',stroke='blue') + #or by updating the attributes attribute: + c.attributes['stroke-width']=1 + s.addElement(c) + #then you add the svg root element to the drawing + d.setSVG(s) + #and finaly you xmlify the drawing + d.toXml() + + +this results in the svg source of the drawing, which consists of a circle +on a white background. Its as easy as that;) +This module was created using the SVG specification of www.w3c.org and the +O'Reilly (www.oreilly.com) python books as information sources. A svg viewer +is available from www.adobe.com""" + +__version__="1.0" + +# there are two possibilities to generate svg: +# via a dom implementation and directly using text strings +# the latter is way faster (and shorter in coding) +# the former is only used in debugging svg programs +# maybe it will be removed alltogether after a while +# with the following variable you indicate whether to use the dom implementation +# Note that PyXML is required for using the dom implementation. +# It is also possible to use the standard minidom. But I didn't try that one. +# Anyway the text based approach is about 60 times faster than using the full dom implementation. +use_dom_implementation=0 + + +import exceptions +if use_dom_implementation<>0: + try: + from xml.dom import implementation + from xml.dom.ext import PrettyPrint + except: + raise exceptions.ImportError, "PyXML is required for using the dom implementation" +#The implementation is used for the creating the XML document. +#The prettyprint module is used for converting the xml document object to a xml file + +import sys +assert sys.version_info[0]>=2 +if sys.version_info[1]<2: + True=1 + False=0 + file=open + +sys.setrecursionlimit=50 +#The recursion limit is set conservative so mistakes like s=svg() s.addElement(s) +#won't eat up too much processor time. + +xlinkNSRef = "http://www.w3.org/1999/xlink" + +#the following code is pasted form xml.sax.saxutils +#it makes it possible to run the code without the xml sax package installed +#To make it possible to have in your text elements, it is necessary to escape the texts +def _escape(data, entities={}): + """Escape &, <, and > in a string of data. + + You can escape other strings of data by passing a dictionary as + the optional entities parameter. The keys and values must all be + strings; each key will be replaced with its corresponding value. + """ + data = data.replace("&", "&") + data = data.replace("<", "<") + data = data.replace(">", ">") + for chars, entity in entities.items(): + data = data.replace(chars, entity) + return data + +def _quoteattr(data, entities={}): + """Escape and quote an attribute value. + + Escape &, <, and > in a string of data, then quote it for use as + an attribute value. The \" character will be escaped as well, if + necessary. + + You can escape other strings of data by passing a dictionary as + the optional entities parameter. The keys and values must all be + strings; each key will be replaced with its corresponding value. + """ + data = _escape(data, entities) + if '"' in data: + if "'" in data: + data = '"%s"' % data.replace('"', """) + else: + data = "'%s'" % data + else: + data = '"%s"' % data + return data + + + +def _xypointlist(a): + """formats a list of xy pairs""" + s='' + for e in a: #this could be done more elegant + s+=str(e)[1:-1] +' ' + return s + +def _viewboxlist(a): + """formats a tuple""" + s='' + for e in a: + s+=str(e)+' ' + return s + +def _pointlist(a): + """formats a list of numbers""" + return str(a)[1:-1] + +class pathdata: + """class used to create a pathdata object which can be used for a path. + although most methods are pretty straightforward it might be useful to look at the SVG specification.""" + #I didn't test the methods below. + def __init__(self,x=None,y=None): + self.path=[] + if x is not None and y is not None: + self.path.append('M '+str(x)+' '+str(y)) + def closepath(self): + """ends the path""" + self.path.append('z') + def move(self,x,y): + """move to absolute""" + self.path.append('M '+str(x)+' '+str(y)) + def relmove(self,x,y): + """move to relative""" + self.path.append('m '+str(x)+' '+str(y)) + def line(self,x,y): + """line to absolute""" + self.path.append('L '+str(x)+' '+str(y)) + def relline(self,x,y): + """line to relative""" + self.path.append('l '+str(x)+' '+str(y)) + def hline(self,x): + """horizontal line to absolute""" + self.path.append('H'+str(x)) + def relhline(self,x): + """horizontal line to relative""" + self.path.append('h'+str(x)) + def vline(self,y): + """verical line to absolute""" + self.path.append('V'+str(y)) + def relvline(self,y): + """vertical line to relative""" + self.path.append('v'+str(y)) + def bezier(self,x1,y1,x2,y2,x,y): + """bezier with xy1 and xy2 to xy absolut""" + self.path.append('C'+str(x1)+','+str(y1)+' '+str(x2)+','+str(y2)+' '+str(x)+','+str(y)) + def relbezier(self,x1,y1,x2,y2,x,y): + """bezier with xy1 and xy2 to xy relative""" + self.path.append('c'+str(x1)+','+str(y1)+' '+str(x2)+','+str(y2)+' '+str(x)+','+str(y)) + def smbezier(self,x2,y2,x,y): + """smooth bezier with xy2 to xy absolut""" + self.path.append('S'+str(x2)+','+str(y2)+' '+str(x)+','+str(y)) + def relsmbezier(self,x2,y2,x,y): + """smooth bezier with xy2 to xy relative""" + self.path.append('s'+str(x2)+','+str(y2)+' '+str(x)+','+str(y)) + def qbezier(self,x1,y1,x,y): + """quadratic bezier with xy1 to xy absolut""" + self.path.append('Q'+str(x1)+','+str(y1)+' '+str(x)+','+str(y)) + def relqbezier(self,x1,y1,x,y): + """quadratic bezier with xy1 to xy relative""" + self.path.append('q'+str(x1)+','+str(y1)+' '+str(x)+','+str(y)) + def smqbezier(self,x,y): + """smooth quadratic bezier to xy absolut""" + self.path.append('T'+str(x)+','+str(y)) + def relsmqbezier(self,x,y): + """smooth quadratic bezier to xy relative""" + self.path.append('t'+str(x)+','+str(y)) + def ellarc(self,rx,ry,xrot,laf,sf,x,y): + """elliptival arc with rx and ry rotating with xrot using large-arc-flag and sweep-flag to xy absolut""" + self.path.append('A'+str(rx)+','+str(ry)+' '+str(xrot)+' '+str(laf)+' '+str(sf)+' '+str(x)+' '+str(y)) + def relellarc(self,rx,ry,xrot,laf,sf,x,y): + """elliptival arc with rx and ry rotating with xrot using large-arc-flag and sweep-flag to xy relative""" + self.path.append('a'+str(rx)+','+str(ry)+' '+str(xrot)+' '+str(laf)+' '+str(sf)+' '+str(x)+' '+str(y)) + def __repr__(self): + return ' '.join(self.path) + + +class Attribute: + def __init__(self, name, value, nsname=None, nsref=None): + self.name = name + self.value = value + self.nsname = nsname + self.nsref = nsref + +class SVGelement: + """SVGelement(type,attributes,elements,text,namespace,**args) + Creates a arbitrary svg element and is intended to be subclassed not used on its own. + This element is the base of every svg element it defines a class which resembles + a xml-element. The main advantage of this kind of implementation is that you don't + have to create a toXML method for every different graph object. Every element + consists of a type, attribute, optional subelements, optional text and an optional + namespace. Note the elements==None, if elements = None:self.elements=[] construction. + This is done because if you default to elements=[] every object has a reference + to the same empty list.""" + def __init__(self,type='',attributes=None,elements=None,text='',namespace='',cdata=None,**args): + self.type=type + self._attributes={} + if attributes: + for key, value in attributes.items(): + attr = Attribute(key, value) + self._attributes[key] = attr + self.elements=[] + if elements: + self.elements=elements + self.text=text + self.namespace=namespace + self.cdata=cdata + for key, value in args.items(): + attr = Attribute(key, value) + self._attributes[key] = attr + self._parent = None + + def addElement(self,SVGelement): + """adds an element to a SVGelement + + SVGelement.addElement(SVGelement) + """ + self.elements.append(SVGelement) + SVGelement.setParent(self) + + def setParent(self, parent): + self._parent = parent + + def addAttribute(self, attribute): + self._attributes[attribute.name] = attribute + + def toXml(self,level,f): + f.write('\t'*level) + f.write('<'+self.type) + if self.namespace: + f.write(' xmlns="'+ _escape(str(self.namespace))+'" ') + for attkey, attr in self._attributes.items(): + if attr.nsname: + f.write(' xmlns:'+_escape(str(attr.nsname))+'="'+_escape(str(attr.nsref))+'" ') + f.write(' '+_escape(str(attr.nsname))+':'+_escape(str(attkey))+'='+_quoteattr(str(attr.value))) + else: + f.write(' '+_escape(str(attkey))+'='+_quoteattr(str(attr.value))) + if self.elements or self.text or self.cdata: + f.write('>') + if self.elements: + f.write('\n') + for element in self.elements: + element.toXml(level+1,f) + if self.cdata: + f.write('\n'+'\t'*(level+1)+'\n') + if self.text: + if type(self.text)==type(''): #If the text is only text + f.write(_escape(str(self.text))) + else: #If the text is a spannedtext class + f.write(str(self.text)) + if self.elements: + f.write('\t'*level+'\n') + elif self.text: + f.write('\n') + elif self.cdata: + f.write('\t'*level+'\n') + else: + f.write('/>\n') + +class tspan(SVGelement): + """ts=tspan(text='',**args) + + a tspan element can be used for applying formatting to a textsection + usage: + ts=tspan('this text is bold') + ts.attributes['font-weight']='bold' + st=spannedtext() + st.addtspan(ts) + t=text(3,5,st) + """ + def __init__(self,text=None,**args): + SVGelement.__init__(self,'tspan',**args) + if self.text<>None: + self.text=text + def __repr__(self): + s="None: + raise ValueError, 'height is required' + if height<>None: + raise ValueError, 'width is required' + else: + raise ValueError, 'both height and width are required' + SVGelement.__init__(self,'rect',{'width':width,'height':height},**args) + if x<>None: + self.addAttribute(Attribute('x', x)) + if y<>None: + self.addAttribute(Attribute('y', y)) + if fill<>None: + self.addAttribute(Attribute('fill', fill)) + if stroke<>None: + self.addAttribute(Attribute('stroke', stroke)) + if stroke_width<>None: + self.addAttribute(Attribute('stroke-width', stroke_width)) + +class ellipse(SVGelement): + """e=ellipse(rx,ry,x,y,fill,stroke,stroke_width,**args) + + an ellipse is defined as a center and a x and y radius. + """ + def __init__(self,cx=None,cy=None,rx=None,ry=None,fill=None,stroke=None,stroke_width=None,**args): + if rx==None or ry== None: + if rx<>None: + raise ValueError, 'rx is required' + if ry<>None: + raise ValueError, 'ry is required' + else: + raise ValueError, 'both rx and ry are required' + SVGelement.__init__(self,'ellipse',{'rx':rx,'ry':ry},**args) + if cx<>None: + self.addAttribute(Attribute('cx', cx)) + if cy<>None: + self.addAttribute(Attribute('cy', cy)) + if fill<>None: + self.addAttribute(Attribute('fill', fill)) + if stroke<>None: + self.addAttribute(Attribute('stroke', stroke)) + if stroke_width<>None: + self.addAttribute(Attribute('stroke-width', stroke_width)) + + +class circle(SVGelement): + """c=circle(x,y,radius,fill,stroke,stroke_width,**args) + + The circle creates an element using a x, y and radius values eg + """ + def __init__(self,cx=None,cy=None,r=None,fill=None,stroke=None,stroke_width=None,**args): + if r==None: + raise ValueError, 'r is required' + SVGelement.__init__(self,'circle',{'r':r},**args) + if cx<>None: + self.addAttribute(Attribute('cx', cx)) + if cy<>None: + self.addAttribute(Attribute('cy', cy)) + if fill<>None: + self.addAttribute(Attribute('fill', fill)) + if stroke<>None: + self.addAttribute(Attribute('stroke', stroke)) + if stroke_width<>None: + self.addAttribute(Attribute('stroke-width', stroke_width)) + +class point(circle): + """p=point(x,y,color) + + A point is defined as a circle with a size 1 radius. It may be more efficient to use a + very small rectangle if you use many points because a circle is difficult to render. + """ + def __init__(self,x,y,fill='black',**args): + circle.__init__(self,x,y,1,fill,**args) + +class line(SVGelement): + """l=line(x1,y1,x2,y2,stroke,stroke_width,**args) + + A line is defined by a begin x,y pair and an end x,y pair + """ + def __init__(self,x1=None,y1=None,x2=None,y2=None,stroke=None,stroke_width=None,**args): + SVGelement.__init__(self,'line',**args) + if x1<>None: + self.addAttribute(Attribute('x1', x1)) + if y1<>None: + self.addAttribute(Attribute('y1', y1)) + if x2<>None: + self.addAttribute(Attribute('x2', x2)) + if y2<>None: + self.addAttribute(Attribute('y2', y2)) + if stroke_width<>None: + self.addAttribute(Attribute('stroke-width', stroke_width)) + if stroke<>None: + self.addAttribute(Attribute('stroke', stroke)) + +class polyline(SVGelement): + """pl=polyline([[x1,y1],[x2,y2],...],fill,stroke,stroke_width,**args) + + a polyline is defined by a list of xy pairs + """ + def __init__(self,points,fill=None,stroke=None,stroke_width=None,**args): + SVGelement.__init__(self,'polyline',{'points':_xypointlist(points)},**args) + if fill<>None: + self.addAttribute(Attribute('fill', fill)) + if stroke_width<>None: + self.addAttribute(Attribute('stroke-width', stroke_width)) + if stroke<>None: + self.addAttribute(Attribute('stroke', stroke)) + +class polygon(SVGelement): + """pl=polyline([[x1,y1],[x2,y2],...],fill,stroke,stroke_width,**args) + + a polygon is defined by a list of xy pairs + """ + def __init__(self,points,fill=None,stroke=None,stroke_width=None,**args): + SVGelement.__init__(self,'polygon',{'points':_xypointlist(points)},**args) + if fill<>None: + self.addAttribute(Attribute('fill', fill)) + if stroke_width<>None: + self.addAttribute(Attribute('stroke-width', stroke_width)) + if stroke<>None: + self.addAttribute(Attribute('stroke', stroke)) + +class path(SVGelement): + """p=path(path,fill,stroke,stroke_width,**args) + + a path is defined by a path object and optional width, stroke and fillcolor + """ + def __init__(self,pathdata,fill=None,stroke=None,stroke_width=None,id=None,**args): + SVGelement.__init__(self,'path',{'d':str(pathdata)},**args) + if stroke<>None: + self.addAttribute(Attribute('stroke', stroke)) + if fill<>None: + self.addAttribute(Attribute('fill', fill)) + if stroke_width<>None: + self.addAttribute(Attribute('stroke-width', stroke_width)) + if id<>None: + self.addAttribute(Attribute('id', id)) + + +class text(SVGelement): + """t=text(x,y,text,font_size,font_family,**args) + + a text element can bge used for displaying text on the screen + """ + def __init__(self,x=None,y=None,text=None,font_size=None,font_family=None,text_anchor=None,**args): + SVGelement.__init__(self,'text',**args) + if x<>None: + self.addAttribute(Attribute('x', x)) + if y<>None: + self.addAttribute(Attribute('y', y)) + if font_size<>None: + self.addAttribute(Attribute('font-size', font_size)) + if font_family<>None: + self.addAttribute(Attribute('font-family', font_family)) + if text<>None: + self.text=text + if text_anchor<>None: + self.addAttribute(Attribute('text-anchor', text_anchor)) + + +class textpath(SVGelement): + """tp=textpath(text,link,**args) + + a textpath places a text on a path which is referenced by a link. + """ + def __init__(self,link,text=None,**args): + SVGelement.__init__(self,'textPath',**args) + self.addAttribute(Attribute('href', link, 'xlink', xlinkNSRef)) + if text<>None: + self.text=text + +class pattern(SVGelement): + """p=pattern(x,y,width,height,patternUnits,**args) + + A pattern is used to fill or stroke an object using a pre-defined + graphic object which can be replicated ("tiled") at fixed intervals + in x and y to cover the areas to be painted. + """ + def __init__(self,x=None,y=None,width=None,height=None,patternUnits=None,**args): + SVGelement.__init__(self,'pattern',**args) + if x<>None: + self.addAttribute(Attribute('x', x)) + if y<>None: + self.addAttribute(Attribute('y', y)) + if width<>None: + self.addAttribute(Attribute('width', width)) + if height<>None: + self.addAttribute(Attribute('height', height)) + if patternUnits<>None: + self.addAttribute(Attribute('patternUnits', patternUnits)) + +class title(SVGelement): + """t=title(text,**args) + + a title is a text element. The text is displayed in the title bar + add at least one to the root svg element + """ + def __init__(self,text=None,**args): + SVGelement.__init__(self,'title',**args) + if text<>None: + self.text=text + +class description(SVGelement): + """d=description(text,**args) + + a description can be added to any element and is used for a tooltip + Add this element before adding other elements. + """ + def __init__(self,text=None,**args): + SVGelement.__init__(self,'desc',**args) + if text<>None: + self.text=text + +class lineargradient(SVGelement): + """lg=lineargradient(x1,y1,x2,y2,id,**args) + + defines a lineargradient using two xy pairs. + stop elements van be added to define the gradient colors. + """ + def __init__(self,x1=None,y1=None,x2=None,y2=None,id=None,**args): + SVGelement.__init__(self,'linearGradient',**args) + if x1<>None: + self.addAttribute(Attribute('x1', x1)) + if y1<>None: + self.addAttribute(Attribute('y1', y1)) + if x2<>None: + self.addAttribute(Attribute('x2', x2)) + if y2<>None: + self.addAttribute(Attribute('y2', y2)) + if id<>None: + self.addAttribute(Attribute('id', id)) + +class radialgradient(SVGelement): + """rg=radialgradient(cx,cy,r,fx,fy,id,**args) + + defines a radial gradient using a outer circle which are defined by a cx,cy and r and by using a focalpoint. + stop elements van be added to define the gradient colors. + """ + def __init__(self,cx=None,cy=None,r=None,fx=None,fy=None,id=None,**args): + SVGelement.__init__(self,'radialGradient',**args) + if cx<>None: + self.addAttribute(Attribute('cx', cx)) + if cy<>None: + self.addAttribute(Attribute('cy', cy)) + if r<>None: + self.addAttribute(Attribute('r', r)) + if fx<>None: + self.addAttribute(Attribute('fx', fx)) + if fy<>None: + self.addAttribute(Attribute('fy', fy)) + if id<>None: + self.addAttribute(Attribute('id', id)) + +class stop(SVGelement): + """st=stop(offset,stop_color,**args) + + Puts a stop color at the specified radius + """ + def __init__(self,offset,stop_color=None,**args): + SVGelement.__init__(self,'stop',{'offset':offset},**args) + if stop_color<>None: + self.addAttribute(Attribute('stop-color', stop_color)) + +class style(SVGelement): + """st=style(type,cdata=None,**args) + + Add a CDATA element to this element for defing in line stylesheets etc.. + """ + def __init__(self,type,cdata=None,**args): + SVGelement.__init__(self,'style',{'type':type},cdata=cdata, **args) + + +class image(SVGelement): + """im=image(url,width,height,x,y,**args) + + adds an image to the drawing. Supported formats are .png, .jpg and .svg. + """ + def __init__(self,url,x=None,y=None,width=None,height=None,**args): + if width==None or height==None: + if width<>None: + raise ValueError, 'height is required' + if height<>None: + raise ValueError, 'width is required' + else: + raise ValueError, 'both height and width are required' + SVGelement.__init__(self,'image',{'width':width,'height':height},**args) + self.addAttribute(Attribute('href', url, 'xlink', xlinkNSRef)) + if x<>None: + self.addAttribute(Attribute('x', x)) + if y<>None: + self.addAttribute(Attribute('y', y)) + +class cursor(SVGelement): + """c=cursor(url,**args) + + defines a custom cursor for a element or a drawing + """ + def __init__(self,url,**args): + SVGelement.__init__(self,'cursor',**args) + self.addAttribute(Attribute('href', url, 'xlink', xlinkNSRef)) + + +class marker(SVGelement): + """m=marker(id,viewbox,refX,refY,markerWidth,markerHeight,**args) + + defines a marker which can be used as an endpoint for a line or other pathtypes + add an element to it which should be used as a marker. + """ + def __init__(self,id=None,viewBox=None,refx=None,refy=None,markerWidth=None,markerHeight=None,**args): + SVGelement.__init__(self,'marker',**args) + if id<>None: + self.addAttribute(Attribute('id', id)) + if viewBox<>None: + self.addAttribute(Attribute('viewBox', _viewboxlist(viewBox))) + if refx<>None: + self.addAttribute(Attribute('refX', refx)) + if refy<>None: + self.addAttribute(Attribute('refY', refy)) + if markerWidth<>None: + self.addAttribute(Attribute('markerWidth', markerWidth)) + if markerHeight<>None: + self.addAttribute(Attribute('markerHeight', markerHeight)) + +class group(SVGelement): + """g=group(id,**args) + + a group is defined by an id and is used to contain elements + g.addElement(SVGelement) + """ + def __init__(self,id=None,**args): + SVGelement.__init__(self,'g',**args) + if id<>None: + self.addAttribute(Attribute('id', id)) + +class symbol(SVGelement): + """sy=symbol(id,viewbox,**args) + + defines a symbol which can be used on different places in your graph using + the use element. A symbol is not rendered but you can use 'use' elements to + display it by referencing its id. + sy.addElement(SVGelement) + """ + + def __init__(self,id=None,viewBox=None,**args): + SVGelement.__init__(self,'symbol',**args) + if id<>None: + self.addAttribute(Attribute('id', id)) + if viewBox<>None: + self.addAttribute(Attribute('viewBox', _viewboxlist(viewBox))) + +class defs(SVGelement): + """d=defs(**args) + + container for defining elements + """ + def __init__(self,**args): + SVGelement.__init__(self,'defs',**args) + +class switch(SVGelement): + """sw=switch(**args) + + Elements added to a switch element which are "switched" by the attributes + requiredFeatures, requiredExtensions and systemLanguage. + Refer to the SVG specification for details. + """ + def __init__(self,**args): + SVGelement.__init__(self,'switch',**args) + + +class use(SVGelement): + """u=use(link,x,y,width,height,**args) + + references a symbol by linking to its id and its position, height and width + """ + def __init__(self,link,x=None,y=None,width=None,height=None,**args): + SVGelement.__init__(self,'use',**args) + self.addAttribute(Attribute('href', link, 'xlink', xlinkNSRef)) + if x<>None: + self.addAttribute(Attribute('x', x)) + if y<>None: + self.addAttribute(Attribute('y', y)) + + if width<>None: + self.addAttribute(Attribute('width', width)) + if height<>None: + self.addAttribute(Attribute('height', height)) + + +class link(SVGelement): + """a=link(url,**args) + + a link is defined by a hyperlink. add elements which have to be linked + a.addElement(SVGelement) + """ + def __init__(self,link='',**args): + SVGelement.__init__(self,'a',**args) + self.addAttribute(Attribute('href', link, 'xlink', xlinkNSRef)) + +class view(SVGelement): + """v=view(id,**args) + + a view can be used to create a view with different attributes""" + def __init__(self,id=None,**args): + SVGelement.__init__(self,'view',**args) + if id<>None: + self.addAttribute(Attribute('id', id)) + +class script(SVGelement): + """sc=script(type,type,cdata,**args) + + adds a script element which contains CDATA to the SVG drawing + + """ + def __init__(self,type,cdata=None,**args): + SVGelement.__init__(self,'script',{'type':type},cdata=cdata,**args) + +class animate(SVGelement): + """an=animate(attribute,from,to,during,**args) + + animates an attribute. + """ + def __init__(self,attribute,fr=None,to=None,dur=None,**args): + SVGelement.__init__(self,'animate',{'attributeName':attribute},**args) + if fr<>None: + self.addAttribute(Attribute('from', fr)) + if to<>None: + self.addAttribute(Attribute('to', to)) + if dur<>None: + self.addAttribute(Attribute('dur', dur)) + +class animateMotion(SVGelement): + """an=animateMotion(pathdata,dur,**args) + + animates a SVGelement over the given path in dur seconds + """ + def __init__(self,pathdata,dur,**args): + SVGelement.__init__(self,'animateMotion',**args) + if pathdata<>None: + self.addAttribute(Attribute('path', str(pathdata))) + if dur<>None: + self.addAttribute(Attribute('dur', dur)) + +class animateTransform(SVGelement): + """antr=animateTransform(type,from,to,dur,**args) + + transform an element from and to a value. + """ + def __init__(self,type=None,fr=None,to=None,dur=None,**args): + SVGelement.__init__(self,'animateTransform',{'attributeName':'transform'},**args) + #As far as I know the attributeName is always transform + if type<>None: + self.addAttribute(Attribute('type', type)) + if fr<>None: + self.addAttribute(Attribute('from', fr)) + if to<>None: + self.addAttribute(Attribute('to', to)) + if dur<>None: + self.addAttribute(Attribute('dur', dur)) +class animateColor(SVGelement): + """ac=animateColor(attribute,type,from,to,dur,**args) + + Animates the color of a element + """ + def __init__(self,attribute,type=None,fr=None,to=None,dur=None,**args): + SVGelement.__init__(self,'animateColor',{'attributeName':attribute},**args) + if type<>None: + self.addAttribute(Attribute('type', type)) + if fr<>None: + self.addAttribute(Attribute('from', fr)) + if to<>None: + self.addAttribute(Attribute('to', to)) + if dur<>None: + self.addAttribute(Attribute('dur', dur)) +class set(SVGelement): + """st=set(attribute,to,during,**args) + + sets an attribute to a value for a + """ + def __init__(self,attribute,to=None,dur=None,**args): + SVGelement.__init__(self,'set',{'attributeName':attribute},**args) + if to<>None: + self.addAttribute(Attribute('to', to)) + if dur<>None: + self.addAttribute(Attribute('dur', dur)) + + + +class svg(SVGelement): + """s=svg(viewbox,width,height,**args) + + a svg or element is the root of a drawing add all elements to a svg element. + You can have different svg elements in one svg file + s.addElement(SVGelement) + + eg + d=drawing() + s=svg((0,0,100,100),'100%','100%') + c=circle(50,50,20) + s.addElement(c) + d.setSVG(s) + d.toXml() + """ + def __init__(self,viewBox=None, width=None, height=None,**args): + SVGelement.__init__(self,'svg',**args) + if viewBox<>None: + self.addAttribute(Attribute('viewBox', _viewboxlist(viewBox))) + if width<>None: + self.addAttribute(Attribute('width', width)) + if height<>None: + self.addAttribute(Attribute('height', height_)) + self.namespace="http://www.w3.org/2000/svg" + +class drawing: + """d=drawing() + + this is the actual SVG document. It needs a svg element as a root. + Use the addSVG method to set the svg to the root. Use the toXml method to write the SVG + source to the screen or to a file + d=drawing() + d.addSVG(svg) + d.toXml(optionalfilename) + """ + + def __init__(self): + self.svg=None + + def setSVG(self,svg): + self.svg=svg + #Voeg een element toe aan de grafiek toe. + + if use_dom_implementation==0: + def toXml(self, filename='',compress=False): + import cStringIO + xml=cStringIO.StringIO() + xml.write("\n") + xml.write("\n") + self.svg.toXml(0,xml) + if not filename: + if compress: + import gzip + f=cStringIO.StringIO() + zf=gzip.GzipFile(fileobj=f,mode='wb') + zf.write(xml.getvalue()) + zf.close() + f.seek(0) + return f.read() + else: + return xml.getvalue() + else: + if filename[-4:]=='svgz': + import gzip + f=gzip.GzipFile(filename=filename,mode="wb", compresslevel=9) + f.write(xml.getvalue()) + f.close() + else: + f=file(filename,'w') + f.write(xml.getvalue()) + f.close() + + else: + def toXml(self,filename='',compress=False): + """drawing.toXml() ---->to the screen + drawing.toXml(filename)---->to the file + writes a svg drawing to the screen or to a file + compresses if filename ends with svgz or if compress is true + """ + doctype = implementation.createDocumentType('svg',"-//W3C//DTD SVG 1.0//EN""",'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd ') + + global root + #root is defined global so it can be used by the appender. Its also possible to use it as an arugument but + #that is a bit messy. + root=implementation.createDocument(None,None,doctype) + #Create the xml document. + global appender + def appender(element,elementroot): + """This recursive function appends elements to an element and sets the attributes + and type. It stops when alle elements have been appended""" + if element.namespace: + e=root.createElementNS(element.namespace,element.type) + else: + e=root.createElement(element.type) + if element.text: + textnode=root.createTextNode(element.text) + e.appendChild(textnode) + for attr in element.attributes.values(): #in element.attributes is supported from python 2.2 + if attr.nsname and attr.nsref: + e.setAttributeNS(attr.nsref, attr.nsname+":"+attr.name, str(attr.value)) + else: + e.setAttribute(attr.name,str(attr.value)) + if element.elements: + for el in element.elements: + e=appender(el,e) + elementroot.appendChild(e) + return elementroot + root=appender(self.svg,root) + if not filename: + import cStringIO + xml=cStringIO.StringIO() + PrettyPrint(root,xml) + if compress: + import gzip + f=cStringIO.StringIO() + zf=gzip.GzipFile(fileobj=f,mode='wb') + zf.write(xml.getvalue()) + zf.close() + f.seek(0) + return f.read() + else: + return xml.getvalue() + else: + try: + if filename[-4:]=='svgz': + import gzip + import cStringIO + xml=cStringIO.StringIO() + PrettyPrint(root,xml) + f=gzip.GzipFile(filename=filename,mode='wb',compresslevel=9) + f.write(xml.getvalue()) + f.close() + else: + f=open(filename,'w') + PrettyPrint(root,f) + f.close() + except: + print "Cannot write SVG file: " + filename + + def validate(self): + try: + import xml.parsers.xmlproc.xmlval + except: + raise exceptions.ImportError,'PyXml is required for validating SVG' + svg=self.toXml() + xv=xml.parsers.xmlproc.xmlval.XMLValidator() + try: + xv.feed(svg) + except: + raise "SVG is not well formed, see messages above" + else: + print "SVG well formed" + + +if __name__=='__main__': + + + d=drawing() + s=svg((0,0,100,100)) + r=rect(-100,-100,300,300,'cyan') + s.addElement(r) + + t=title('SVGdraw Demo') + s.addElement(t) + g=group('animations') + e=ellipse(0,0,5,2) + g.addElement(e) + c=circle(0,0,1,'red') + g.addElement(c) + pd=pathdata(0,-10) + for i in range(6): + pd.relsmbezier(10,5,0,10) + pd.relsmbezier(-10,5,0,10) + an=animateMotion(pd,10) + an.addAttribute(Attribute('rotate', 'auto-reverse')) + an.addAttribute(Attribute('repeatCount', "indefinite")) + g.addElement(an) + s.addElement(g) + for i in range(20,120,20): + u=use('#animations',i,0) + s.addElement(u) + for i in range(0,120,20): + for j in range(5,105,10): + c=circle(i,j,1,'red','black',.5) + s.addElement(c) + d.setSVG(s) + + print d.toXml() + diff --git a/sugar/chat/bubble.png b/sugar/chat/bubble.png new file mode 100644 index 00000000..3d1503df Binary files /dev/null and b/sugar/chat/bubble.png differ diff --git a/sugar/chat/bubbleOutline.png b/sugar/chat/bubbleOutline.png new file mode 100644 index 00000000..334ddca9 Binary files /dev/null and b/sugar/chat/bubbleOutline.png differ diff --git a/sugar/chat/chat.py b/sugar/chat/chat.py new file mode 100755 index 00000000..2bc72305 --- /dev/null +++ b/sugar/chat/chat.py @@ -0,0 +1,405 @@ +#!/usr/bin/python -t +# -*- tab-width: 4; indent-tabs-mode: t -*- + +import dbus +import dbus.service +import dbus.glib + +import pygtk +pygtk.require('2.0') +import gtk, gobject + +from sugar.shell import activity +from sugar.p2p.Group import * +from sugar.p2p.StreamReader import * +from sugar.p2p.StreamWriter import * +import sugar.env + +import richtext + +CHAT_SERVICE_TYPE = "_olpc_chat._tcp" +CHAT_SERVICE_PORT = 6100 + +GROUP_CHAT_SERVICE_TYPE = "_olpc_group_chat._udp" +GROUP_CHAT_SERVICE_ADDRESS = "224.0.0.221" +GROUP_CHAT_SERVICE_PORT = 6200 + +class Chat(activity.Activity): + def __init__(self, controller): + self._controller = controller + activity.Activity.__init__(self) + + def activity_on_connected_to_shell(self): + self.activity_set_tab_text(self._act_name) + self._plug = self.activity_get_gtk_plug() + self._ui_setup(self._plug) + self._plug.show_all() + + def _create_chat(self): + chat_vbox = gtk.VBox() + chat_vbox.set_spacing(6) + + sw = gtk.ScrolledWindow() + sw.set_shadow_type(gtk.SHADOW_IN) + sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) + self._chat_view = richtext.RichTextView() + self._chat_view.connect("link-clicked", self.__link_clicked_cb) + self._chat_view.set_editable(False) + self._chat_view.set_cursor_visible(False) + sw.add(self._chat_view) + self._chat_view.show() + chat_vbox.pack_start(sw) + sw.show() + + chat_view_sw = gtk.ScrolledWindow() + chat_view_sw.set_shadow_type(gtk.SHADOW_IN) + chat_view_sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + self._editor = richtext.RichTextView() + self._editor.connect("key-press-event", self.__key_press_event_cb) + self._editor.set_size_request(-1, 50) + chat_view_sw.add(self._editor) + self._editor.show() + + chat_vbox.pack_start(chat_view_sw, False) + chat_view_sw.show() + + return chat_vbox, self._editor.get_buffer() + + def _ui_setup(self, base): + vbox = gtk.VBox(False, 6) + + self._hbox = gtk.HBox(False, 12) + self._hbox.set_border_width(12) + + [chat_vbox, buffer] = self._create_chat() + self._hbox.pack_start(chat_vbox) + chat_vbox.show() + + vbox.pack_start(self._hbox) + self._hbox.show() + + toolbar = self._create_toolbar(buffer) + vbox.pack_start(toolbar, False) + toolbar.show() + + base.add(vbox) + vbox.show() + + def __link_clicked_cb(self, view, address): + self._browser_shell.open_browser(address) + + def __key_press_event_cb(self, text_view, event): + if event.keyval == gtk.keysyms.Return: + buf = text_view.get_buffer() + + serializer = richtext.RichTextSerializer() + text = serializer.serialize(buf) + self.send_message(text) + + buf.set_text("") + buf.place_cursor(buf.get_start_iter()) + + return True + + def _create_toolbar(self, rich_buf): + toolbar = richtext.RichTextToolbar(rich_buf) + + item = gtk.MenuToolButton(None, "Links") + item.set_menu(gtk.Menu()) + item.connect("show-menu", self.__show_link_menu_cb) + toolbar.insert(item, -1) + item.show() + + return toolbar + + def __link_activate_cb(self, item, link): + buf = self._editor.get_buffer() + buf.append_link(link['title'], link['address']) + + def __show_link_menu_cb(self, button): + menu = gtk.Menu() + + links = self._browser_shell.get_links() + + for link in links: + item = gtk.MenuItem(link['title'], False) + item.connect("activate", self.__link_activate_cb, link) + menu.append(item) + item.show() + + button.set_menu(menu) + + def activity_on_close_from_user(self): + print "act %d: in activity_on_close_from_user"%self.activity_get_id() + self.activity_shutdown() + + def activity_on_lost_focus(self): + print "act %d: in activity_on_lost_focus"%self.activity_get_id() + + def activity_on_got_focus(self): + print "act %d: in activity_on_got_focus"%self.activity_get_id() + self._controller.notify_activate(self) + + def recv_message(self, buddy, msg): + self._insert_rich_message(buddy.get_nick_name(), msg) + self._controller.notify_new_message(self, buddy) + + def _insert_rich_message(self, nick, msg): + buffer = self._chat_view.get_buffer() + aniter = buffer.get_end_iter() + buffer.insert(aniter, nick + ": ") + + serializer = richtext.RichTextSerializer() + serializer.deserialize(msg, buffer) + + aniter = buffer.get_end_iter() + buffer.insert(aniter, "\n") + + def _local_message(self, success, text): + if not success: + message = "Error: %s\n" % text + buffer = self._chat_view.get_buffer() + aniter = buffer.get_end_iter() + buffer.insert(aniter, message) + else: + owner = self._controller.get_group().get_owner() + self._insert_rich_message(owner.get_nick_name(), text) + +class BuddyChat(Chat): + def __init__(self, controller, buddy): + self._buddy = buddy + self._act_name = "Chat: %s" % buddy.get_nick_name() + Chat.__init__(self, controller) + + def _start(self): + group = self._controller.get_group() + buddy_name = self._buddy.get_service_name() + service = group.get_service(buddy_name, CHAT_SERVICE_TYPE) + self._stream_writer = StreamWriter(group, service) + + def activity_on_connected_to_shell(self): + Chat.activity_on_connected_to_shell(self) + self.activity_set_can_close(True) + self.activity_set_tab_icon_name("im") + self.activity_show_icon(True) + self._start() + + def recv_message(self, sender, msg): + Chat.recv_message(self, self._buddy, msg) + + def send_message(self, text): + if len(text) > 0: + self._stream_writer.write(text) + self._local_message(True, text) + + def activity_on_close_from_user(self): + Chat.activity_on_close_from_user(self) + del self._chats[self._buddy] + +class GroupChat(Chat): + + _MODEL_COL_NICK = 0 + _MODEL_COL_ICON = 1 + _MODEL_COL_BUDDY = 2 + + def __init__(self): + self._act_name = "Chat" + self._chats = {} + + bus = dbus.SessionBus() + proxy_obj = bus.get_object('com.redhat.Sugar.Browser', '/com/redhat/Sugar/Browser') + self._browser_shell = dbus.Interface(proxy_obj, 'com.redhat.Sugar.BrowserShell') + + Chat.__init__(self, self) + + def get_group(self): + return self._group + + def _start(self): + self._group = LocalGroup() + self._group.add_presence_listener(self._on_group_event) + self._group.join() + + name = self._group.get_owner().get_service_name() + service = Service(name, CHAT_SERVICE_TYPE, '', CHAT_SERVICE_PORT) + self._buddy_reader = StreamReader(self._group, service) + self._buddy_reader.set_listener(self._buddy_recv_message) + service.register(self._group) + + service = Service(name, GROUP_CHAT_SERVICE_TYPE, + GROUP_CHAT_SERVICE_ADDRESS, + GROUP_CHAT_SERVICE_PORT, True) + self._group.add_service(service) + + self._buddy_reader = StreamReader(self._group, service) + self._buddy_reader.set_listener(self.recv_message) + + self._stream_writer = StreamWriter(self._group, service) + + def _create_sidebar(self): + vbox = gtk.VBox(False, 6) + + label = gtk.Label("Who's around:") + label.set_alignment(0.0, 0.5) + vbox.pack_start(label, False) + label.show() + + self._buddy_list_model = gtk.ListStore(gobject.TYPE_STRING, gtk.gdk.Pixbuf, gobject.TYPE_PYOBJECT) + + image_path = sugar.env.get_data_file('bubbleOutline.png') + self._pixbuf_active_chat = gtk.gdk.pixbuf_new_from_file(image_path) + + image_path = sugar.env.get_data_file('bubble.png') + self._pixbuf_new_message = gtk.gdk.pixbuf_new_from_file(image_path) + + sw = gtk.ScrolledWindow() + sw.set_shadow_type(gtk.SHADOW_IN) + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + + self._buddy_list_view = gtk.TreeView(self._buddy_list_model) + self._buddy_list_view.set_headers_visible(False) + self._buddy_list_view.connect("cursor-changed", self._on_buddyList_buddy_selected) + self._buddy_list_view.connect("row-activated", self._on_buddyList_buddy_double_clicked) + + sw.set_size_request(120, -1) + sw.add(self._buddy_list_view) + self._buddy_list_view.show() + + renderer = gtk.CellRendererPixbuf() + column = gtk.TreeViewColumn("", renderer, pixbuf=self._MODEL_COL_ICON) + column.set_resizable(False) + column.set_expand(False); + self._buddy_list_view.append_column(column) + + renderer = gtk.CellRendererText() + column = gtk.TreeViewColumn("", renderer, text=self._MODEL_COL_NICK) + column.set_resizable(True) + column.set_sizing("GTK_TREE_VIEW_COLUMN_GROW_ONLY"); + column.set_expand(True); + self._buddy_list_view.append_column(column) + + vbox.pack_start(sw) + sw.show() + + return vbox + + def _ui_setup(self, base): + Chat._ui_setup(self, base) + + sidebar = self._create_sidebar() + self._hbox.pack_start(sidebar, False) + sidebar.show() + self._plug.show_all() + + def activity_on_connected_to_shell(self): + Chat.activity_on_connected_to_shell(self) + + self.activity_set_tab_icon_name("stock_help-chat") + self.activity_show_icon(True) + + aniter = self._buddy_list_model.append(None) + self._buddy_list_model.set(aniter, self._MODEL_COL_NICK, "Group", + self._MODEL_COL_ICON, self._pixbuf_active_chat, self._MODEL_COL_BUDDY, None) + self._start() + + def activity_on_disconnected_from_shell(self): + Chat.activity_on_disconnected_from_shell(self) + gtk.main_quit() + + def _on_buddyList_buddy_selected(self, widget, *args): + (model, aniter) = widget.get_selection().get_selected() + name = self._buddy_list_model.get(aniter, self._MODEL_COL_NICK) + print "Selected %s" % name + + def _on_buddyList_buddy_double_clicked(self, widget, *args): + """ Select the chat for this buddy or group """ + (model, aniter) = widget.get_selection().get_selected() + chat = None + buddy = self._buddy_list_model.get_value(aniter, self._MODEL_COL_BUDDY) + if buddy and not self._chats.has_key(buddy): + chat = BuddyChat(self, buddy) + self._chats[buddy] = chat + chat.activity_connect_to_shell() + + def _on_group_event(self, action, buddy): + if buddy.get_nick_name() == self._group.get_owner().get_nick_name(): + # Do not show ourself in the buddy list + pass + elif action == BUDDY_JOIN: + aniter = self._buddy_list_model.append(None) + self._buddy_list_model.set(aniter, self._MODEL_COL_NICK, buddy.get_nick_name(), + self._MODEL_COL_ICON, None, self._MODEL_COL_BUDDY, buddy) + elif action == BUDDY_LEAVE: + aniter = self._get_iter_for_buddy(buddy) + if aniter: + self._buddy_list_model.remove(aniter) + + def _get_iter_for_buddy(self, buddy): + aniter = self._buddy_list_model.get_iter_first() + while aniter: + list_buddy = self._buddy_list_model.get_value(aniter, self._MODEL_COL_BUDDY) + if buddy == list_buddy: + return aniter + aniter = self._buddy_list_model.iter_next(aniter) + + def notify_new_message(self, chat, buddy): + aniter = self._get_iter_for_buddy(buddy) + self._buddy_list_model.set(aniter, self._MODEL_COL_ICON, self._pixbuf_new_message) + + def notify_activate(self, chat): + aniter = self._get_iter_for_buddy(buddy) + self._buddy_list_model.set(aniter, self._MODEL_COL_ICON, self._pixbuf_active_chat) + + def send_message(self, text): + if len(text) > 0: + self._stream_writer.write(text) + self._local_message(True, text) + + def recv_message(self, buddy, msg): + if buddy: + self._insert_rich_message(buddy.get_nick_name(), msg) + self._controller.notify_new_message(self, None) + + def _buddy_recv_message(self, sender, msg): + if not self._chats.has_key(sender): + chat = BuddyChat(self, sender) + self._chats[sender] = chat + chat.activity_connect_to_shell() + else: + chat = self._chats[sender] + chat.recv_message(sender, msg) + +class ChatShell(dbus.service.Object): + instance = None + + def get_instance(): + if not ChatShell.instance: + ChatShell.instance = ChatShell() + return ChatShell.instance + + get_instance = staticmethod(get_instance) + + def __init__(self): + session_bus = dbus.SessionBus() + bus_name = dbus.service.BusName('com.redhat.Sugar.Chat', bus=session_bus) + object_path = '/com/redhat/Sugar/Chat' + + dbus.service.Object.__init__(self, bus_name, object_path) + + def open_group_chat(self): + group_chat = GroupChat() + group_chat.activity_connect_to_shell() + + @dbus.service.method('com.redhat.Sugar.ChatShell') + def send_message(self, message): + pass + +def main(): + ChatShell.get_instance().open_group_chat() + try: + gtk.main() + except KeyboardInterrupt: + pass + +if __name__ == "__main__": + main() diff --git a/sugar/chat/richtext.py b/sugar/chat/richtext.py new file mode 100644 index 00000000..0ac70b16 --- /dev/null +++ b/sugar/chat/richtext.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python + +import pygtk +import gobject +pygtk.require('2.0') +import gtk +import pango +import xml.sax + +class RichTextView(gtk.TextView): + + __gsignals__ = { + 'link-clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_STRING])) + } + + def __init__(self): + gtk.TextView.__init__(self, RichTextBuffer()) + self.connect("motion-notify-event", self.__motion_notify_cb) + self.connect("button-press-event", self.__button_press_cb) + self.__hover_link = False + + def _set_hover_link(self, hover_link): + if hover_link != self.__hover_link: + self.__hover_link = hover_link + display = self.get_toplevel().get_display() + child_window = self.get_window(gtk.TEXT_WINDOW_TEXT) + + if hover_link: + cursor = gtk.gdk.Cursor(display, gtk.gdk.HAND2) + else: + cursor = gtk.gdk.Cursor(display, gtk.gdk.XTERM) + + child_window.set_cursor(cursor) + gtk.gdk.flush() + + def __iter_is_link(self, it): + item = self.get_buffer().get_tag_table().lookup("link") + if item: + return it.has_tag(item) + return False + + def __get_event_iter(self, event): + return self.get_iter_at_location(int(event.x), int(event.y)) + + def __motion_notify_cb(self, widget, event): + if event.is_hint: + [x, y, state] = event.window.get_pointer(); + + it = self.__get_event_iter(event) + if it: + hover_link = self.__iter_is_link(it) + else: + hover_link = False + + self._set_hover_link(hover_link) + + def __button_press_cb(self, widget, event): + it = self.__get_event_iter(event) + if it and self.__iter_is_link(it): + buf = self.get_buffer() + address_tag = buf.get_tag_table().lookup("link-address") + + address_end = it.copy() + address_end.backward_to_tag_toggle(address_tag) + + address_start = address_end.copy() + address_start.backward_to_tag_toggle(address_tag) + + address = buf.get_text(address_start, address_end) + self.emit("link-clicked", address) + +class RichTextBuffer(gtk.TextBuffer): + def __init__(self): + gtk.TextBuffer.__init__(self) + + self.connect_after("insert-text", self.__insert_text_cb) + + self.__create_tags() + self.active_tags = [] + + def append_link(self, title, address): + it = self.get_iter_at_mark(self.get_insert()) + self.insert_with_tags_by_name(it, address, "link", "link-address") + self.insert_with_tags_by_name(it, title, "link") + + def apply_tag(self, tag_name): + self.active_tags.append(tag_name) + + bounds = self.get_selection_bounds() + if bounds: + [start, end] = bounds + self.apply_tag_by_name(tag_name, start, end) + + def unapply_tag(self, tag_name): + self.active_tags.remove(tag_name) + + bounds = self.get_selection_bounds() + if bounds: + [start, end] = bounds + self.remove_tag_by_name(tag_name, start, end) + + def __create_tags(self): + tag = self.create_tag("link") + tag.set_property("underline", pango.UNDERLINE_SINGLE) + tag.set_property("foreground", "#0000FF") + + tag = self.create_tag("link-address") + tag.set_property("invisible", True) + + tag = self.create_tag("bold") + tag.set_property("weight", pango.WEIGHT_BOLD) + + tag = self.create_tag("italic") + tag.set_property("style", pango.STYLE_ITALIC) + + tag = self.create_tag("font-size-xx-small") + tag.set_property("scale", pango.SCALE_XX_SMALL) + + tag = self.create_tag("font-size-x-small") + tag.set_property("scale", pango.SCALE_X_SMALL) + + tag = self.create_tag("font-size-small") + tag.set_property("scale", pango.SCALE_SMALL) + + tag = self.create_tag("font-size-large") + tag.set_property("scale", pango.SCALE_LARGE) + + tag = self.create_tag("font-size-x-large") + tag.set_property("scale", pango.SCALE_X_LARGE) + + tag = self.create_tag("font-size-xx-large") + tag.set_property("scale", pango.SCALE_XX_LARGE) + + def __insert_text_cb(self, widget, pos, text, length): + for tag in self.active_tags: + pos_end = pos.copy() + pos_end.backward_chars(length) + self.apply_tag_by_name(tag, pos, pos_end) + +class RichTextToolbar(gtk.Toolbar): + def __init__(self, buf): + gtk.Toolbar.__init__(self) + + self.buf = buf + + self.set_style(gtk.TOOLBAR_ICONS) + + self._font_size = "normal" + self._font_scales = [ "xx-small", "x-small", "small", \ + "normal", \ + "large", "x-large", "xx-large" ] + + item = gtk.ToggleToolButton(gtk.STOCK_BOLD) + item.connect("toggled", self.__toggle_style_cb, "bold") + self.insert(item, -1) + item.show() + + item = gtk.ToggleToolButton(gtk.STOCK_ITALIC) + item.connect("toggled", self.__toggle_style_cb, "italic") + self.insert(item, -1) + item.show() + + self._font_size_up = gtk.ToolButton(gtk.STOCK_GO_UP) + self._font_size_up.connect("clicked", self.__font_size_up_cb) + self.insert(self._font_size_up, -1) + self._font_size_up.show() + + self._font_size_down = gtk.ToolButton(gtk.STOCK_GO_DOWN) + self._font_size_down.connect("clicked", self.__font_size_down_cb) + self.insert(self._font_size_down, -1) + self._font_size_down.show() + + def _get_font_size_index(self): + return self._font_scales.index(self._font_size); + + def __toggle_style_cb(self, toggle, tag_name): + if toggle.get_active(): + self.buf.apply_tag(tag_name) + else: + self.buf.unapply_tag(tag_name) + + def _set_font_size(self, font_size): + if self._font_size != "normal": + self.buf.unapply_tag("font-size-" + self._font_size) + if font_size != "normal": + self.buf.apply_tag("font-size-" + font_size) + + self._font_size = font_size + + can_up = self._get_font_size_index() < len(self._font_scales) - 1 + can_down = self._get_font_size_index() > 0 + self._font_size_up.set_sensitive(can_up) + self._font_size_down.set_sensitive(can_down) + + def __font_size_up_cb(self, button): + index = self._get_font_size_index() + if index + 1 < len(self._font_scales): + self._set_font_size(self._font_scales[index + 1]) + + def __font_size_down_cb(self, button): + index = self._get_font_size_index() + if index > 0: + self._set_font_size(self._font_scales[index - 1]) + +class RichTextHandler(xml.sax.handler.ContentHandler): + def __init__(self, serializer, buf): + self.buf = buf + self.serializer = serializer + self.tags = [] + + def startElement(self, name, attrs): + if name != "richtext": + tag = self.serializer.deserialize_element(name, attrs) + self.tags.append(tag) + if name == "link": + self.href = attrs['href'] + + def characters(self, data): + start_it = it = self.buf.get_end_iter() + mark = self.buf.create_mark(None, start_it, True) + self.buf.insert(it, data) + start_it = self.buf.get_iter_at_mark(mark) + + for tag in self.tags: + self.buf.apply_tag_by_name(tag, start_it, it) + if tag == "link": + self.buf.insert_with_tags_by_name(start_it, self.href, + "link", "link-address") + + def endElement(self, name): + if name != "richtext": + self.tags.pop() + +class RichTextSerializer: + def __init__(self): + self._open_tags = [] + + def deserialize_element(self, el_name, attributes): + if el_name == "bold": + return "bold" + elif el_name == "italic": + return "italic" + elif el_name == "font": + return "font-size-" + attributes["size"] + elif el_name == "link": + return "link" + else: + return None + + def serialize_tag_start(self, tag, it): + name = tag.get_property("name") + if name == "bold": + return "" + elif name == "italic": + return "" + elif name == "link": + address_tag = self.buf.get_tag_table().lookup("link-address") + end = it.copy() + end.forward_to_tag_toggle(address_tag) + address = self.buf.get_text(it, end) + return "" + elif name == "link-address": + return "" + elif name.startswith("font-size-"): + tag_name = name.replace("font-size-", "", 1) + return "" + else: + return "" + + def serialize_tag_end(self, tag): + name = tag.get_property("name") + if name == "bold": + return "" + elif name == "italic": + return "" + elif name == "link": + return "" + elif name == "link-address": + return "" + elif name.startswith("font-size-"): + return "" + else: + return "" + + def serialize(self, buf): + self.buf = buf + + xml = "" + + next_it = buf.get_start_iter() + while not next_it.is_end(): + it = next_it.copy() + if not next_it.forward_to_tag_toggle(None): + next_it = buf.get_end_iter() + + tags_to_reopen = [] + + for tag in it.get_toggled_tags(False): + while 1: + open_tag = self._open_tags.pop() + xml += self.serialize_tag_end(tag) + if open_tag == tag: + break + tags_to_reopen.append(open_tag) + + for tag in tags_to_reopen: + self._open_tags.append(tag) + xml += self.serialize_tag_start(tag, it) + + for tag in it.get_toggled_tags(True): + self._open_tags.append(tag) + xml += self.serialize_tag_start(tag, it) + + xml += buf.get_text(it, next_it, False) + + if next_it.is_end(): + self._open_tags.reverse() + for tag in self._open_tags: + xml += self.serialize_tag_end(tag) + + xml += "" + + return xml + + def deserialize(self, xml_string, buf): + parser = xml.sax.make_parser() + handler = RichTextHandler(self, buf) + parser.setContentHandler(handler) + parser.feed(xml_string) + parser.close() + +def test_quit(window, rich_buf): + print RichTextSerializer().serialize(rich_buf) + gtk.main_quit() + +def link_clicked(view, address): + print "Link clicked " + address + +if __name__ == "__main__": + window = gtk.Window() + window.set_default_size(400, 300) + + vbox = gtk.VBox() + + view = RichTextView() + view.connect("link-clicked", link_clicked) + vbox.pack_start(view) + view.show() + + rich_buf = view.get_buffer() + + xml_string = "" + + xml_string += "Testone\n" + xml_string += "Test two" + xml_string += "Test three" + xml_string += "Test link" + xml_string += "" + + RichTextSerializer().deserialize(xml_string, rich_buf) + + toolbar = RichTextToolbar(rich_buf) + vbox.pack_start(toolbar, False) + toolbar.show() + + window.add(vbox) + vbox.show() + + window.show() + + window.connect("destroy", test_quit, rich_buf) + + gtk.main() diff --git a/sugar/p2p/Buddy.py b/sugar/p2p/Buddy.py new file mode 100644 index 00000000..19d7c0ef --- /dev/null +++ b/sugar/p2p/Buddy.py @@ -0,0 +1,34 @@ +import pwd +import os + +from Service import * + +PRESENCE_SERVICE_TYPE = "_olpc_presence._tcp" +PRESENCE_SERVICE_PORT = 6000 + +class Buddy: + def __init__(self, service, nick_name): + self._service = service + self._nick_name = nick_name + + def get_service_name(self): + return self._service.get_name() + + def get_nick_name(self): + return self._nick_name + +class Owner(Buddy): + def __init__(self, group): + self._group = group + + nick = pwd.getpwuid(os.getuid())[0] + if not nick or not len(nick): + nick = "n00b" + + service = Service(nick, PRESENCE_SERVICE_TYPE, + '', PRESENCE_SERVICE_PORT) + + Buddy.__init__(self, service, nick) + + def register(self): + self._service.register(self._group) diff --git a/sugar/p2p/Group.py b/sugar/p2p/Group.py new file mode 100644 index 00000000..dedbc1ed --- /dev/null +++ b/sugar/p2p/Group.py @@ -0,0 +1,102 @@ +import avahi + +import presence +from Buddy import * +from Service import * + +SERVICE_ADDED = "service_added" +SERVICE_REMOVED = "service_removed" + +BUDDY_JOIN = "buddy_join" +BUDDY_LEAVE = "buddy_leave" + +class Group: + def __init__(self): + self._service_listeners = [] + self._presence_listeners = [] + + def join(self, buddy): + pass + + def add_service_listener(self, listener): + self._service_listeners.append(listener) + + def add_presence_listener(self, listener): + self._presence_listeners.append(listener) + + def _notify_service_added(self, service): + for listener in self._service_listeners: + listener(SERVICE_ADDED, buddy) + + def _notify_service_removed(self, service): + for listener in self._service_listeners: + listener(SERVICE_REMOVED,buddy) + + def _notify_buddy_join(self, buddy): + for listener in self._presence_listeners: + listener(BUDDY_JOIN, buddy) + + def _notify_buddy_leave(self, buddy): + for listener in self._presence_listeners: + listener(BUDDY_LEAVE, buddy) + +class LocalGroup(Group): + def __init__(self): + Group.__init__(self) + + self._services = {} + self._buddies = {} + + self._pdiscovery = presence.PresenceDiscovery() + self._pdiscovery.add_service_listener(self._on_service_change) + self._pdiscovery.start() + + def get_owner(self): + return self._owner + + def add_service(self, service): + sid = (service.get_name(), service.get_type()) + self._services[sid] = service + self._notify_service_added(service) + + def remove_service(self, sid): + self._notify_service_removed(service) + del self._services[sid] + + def join(self): + self._owner = Owner(self) + self._owner.register() + + def get_service(self, name, stype): + return self._services[(name, stype)] + + def get_buddy(self, name): + return self._buddies[name] + + def _add_buddy(self, buddy): + bid = buddy.get_nick_name() + if not self._buddies.has_key(bid): + self._buddies[bid] = buddy + self._notify_buddy_join(buddy) + + def _remove_buddy(self, buddy): + self._notify_buddy_leave(buddy) + del self._buddies[buddy.get_nick_name()] + + def _on_service_change(self, action, interface, protocol, name, stype, domain, flags): + if action == presence.ACTION_SERVICE_NEW: + self._pdiscovery.resolve_service(interface, protocol, name, stype, domain, + self._on_service_resolved) + elif action == presence.ACTION_SERVICE_REMOVED: + if stype == PRESENCE_SERVICE_TYPE: + self._remove_buddy(name) + elif stype.startswith("_olpc"): + self.remove_service((name, stype)) + + def _on_service_resolved(self, interface, protocol, name, stype, domain, + host, aprotocol, address, port, txt, flags): + service = Service(name, stype, address, port) + if stype == PRESENCE_SERVICE_TYPE: + self._add_buddy(Buddy(service, name)) + elif stype.startswith("_olpc"): + self.add_service(service) diff --git a/sugar/p2p/Makefile.am b/sugar/p2p/Makefile.am new file mode 100644 index 00000000..be343601 --- /dev/null +++ b/sugar/p2p/Makefile.am @@ -0,0 +1,10 @@ +sugardir = $(pythondir)/sugar/p2p +sugar_PYTHON = \ + __init__.py \ + Buddy.py \ + Group.py \ + Service.py \ + StreamReader.py \ + StreamWriter.py \ + network.py \ + presence.py diff --git a/sugar/p2p/Service.py b/sugar/p2p/Service.py new file mode 100644 index 00000000..50bbf86c --- /dev/null +++ b/sugar/p2p/Service.py @@ -0,0 +1,31 @@ +import presence + +class Service(object): + def __init__(self, name, stype, address, port, multicast=False): + self._name = name + self._stype = stype + self._address = str(address) + self._port = int(port) + self._multicast = multicast + + def get_name(self): + return self._name + + def get_type(self): + return self._stype + + def get_address(self): + return self._address + + def get_port(self): + return self._port + + def set_port(self, port): + self._port = port + + def is_multicast(self): + return self._multicast + + def register(self, group): + pannounce = presence.PresenceAnnounce() + pannounce.register_service(self._name, self._port, self._stype) diff --git a/sugar/p2p/StreamReader.py b/sugar/p2p/StreamReader.py new file mode 100644 index 00000000..c108547b --- /dev/null +++ b/sugar/p2p/StreamReader.py @@ -0,0 +1,51 @@ +import network + +class StreamReaderRequestHandler(object): + def __init__(self, reader): + self._reader = reader + + def message(self, nick_name, message): + address = network.get_authinfo() + self._reader.recv(nick_name, message) + return True + +class StreamReader: + def __init__(self, group, service): + self._group = group + self._service = service + + if self._service.is_multicast(): + self._setup_multicast() + else: + self._setup_unicast() + + def set_listener(self, callback): + self._callback = callback + + def _setup_multicast(self): + address = self._service.get_address() + port = self._service.get_port() + server = network.GroupServer(address, port, self._recv_multicast) + server.start() + + def _setup_unicast(self): + started = False + tries = 10 + port = self._service.get_port() + while not started and tries > 0: + try: + p2p_server = network.GlibXMLRPCServer(("", port)) + p2p_server.register_instance(StreamReaderRequestHandler(self)) + started = True + except: + port = port + 1 + tries = tries - 1 + self._service.set_port(port) + + def _recv_multicast(self, msg): + [ nick_name, data ] = msg['data'].split(" |**| ", 2) + self.recv(nick_name, data) + + def recv(self, nick_name, data): + if nick_name != self._group.get_owner().get_nick_name(): + self._callback(self._group.get_buddy(nick_name), data) diff --git a/sugar/p2p/StreamWriter.py b/sugar/p2p/StreamWriter.py new file mode 100644 index 00000000..f30801ea --- /dev/null +++ b/sugar/p2p/StreamWriter.py @@ -0,0 +1,43 @@ +import xmlrpclib +import traceback +import socket + +import network + +class StreamWriter: + def __init__(self, group, service): + self._group = group + self._service = service + self._address = self._service.get_address() + self._port = self._service.get_port() + + if self._service.is_multicast(): + self._setup_multicast() + else: + self._setup_unicast() + + def write(self, data): + if self._service.is_multicast(): + self._multicast_write(data) + else: + self._unicast_write(data) + + def _setup_unicast(self): + xmlrpc_addr = "http://%s:%d" % (self._address, self._port) + self._uclient = xmlrpclib.ServerProxy(xmlrpc_addr) + + def _unicast_write(self, data): + try: + nick_name = self._group.get_owner().get_nick_name() + self._uclient.message(nick_name, data) + return True + except (socket.error, xmlrpclib.Fault, xmlrpclib.ProtocolError), e: + traceback.print_exc() + return False + + def _setup_multicast(self): + self._mclient = network.GroupClient(self._address, self._port) + + def _multicast_write(self, data): + nick_name = self._group.get_owner().get_nick_name() + self._mclient.send_msg(nick_name + " |**| " + data) diff --git a/sugar/p2p/__init__.py b/sugar/p2p/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sugar/p2p/network.py b/sugar/p2p/network.py new file mode 100644 index 00000000..c88ede6c --- /dev/null +++ b/sugar/p2p/network.py @@ -0,0 +1,176 @@ +# -*- tab-width: 4; indent-tabs-mode: t -*- + +import socket +import threading +import traceback +import select +import time +import xmlrpclib +import sys + +import gobject +import SimpleXMLRPCServer +import SocketServer + +__authinfos = {} + +def _add_authinfo(authinfo): + __authinfos[threading.currentThread()] = authinfo + +def get_authinfo(): + return __authinfos.get(threading.currentThread()) + +def _del_authinfo(): + del __authinfos[threading.currentThread()] + + +class GlibTCPServer(SocketServer.TCPServer): + """GlibTCPServer + + Integrate socket accept into glib mainloop. + """ + + allow_reuse_address = True + request_queue_size = 20 + + def __init__(self, server_address, RequestHandlerClass): + SocketServer.TCPServer.__init__(self, server_address, RequestHandlerClass) + self.socket.setblocking(0) # Set nonblocking + + # Watch the listener socket for data + gobject.io_add_watch(self.socket, gobject.IO_IN, self._handle_accept) + + def _handle_accept(self, source, condition): + if not (condition & gobject.IO_IN): + return True + self.handle_request() + return True + +class GlibXMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): + """ GlibXMLRPCRequestHandler + + The stock SimpleXMLRPCRequestHandler and server don't allow any way to pass + the client's address and/or SSL certificate into the function that actually + _processes_ the request. So we have to store it in a thread-indexed dict. + """ + + def do_POST(self): + _add_authinfo(self.client_address) + try: + SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.do_POST(self) + except socket.timeout: + pass + except socket.error, e: + print "Error (%s): socket error - '%s'" % (self.client_address, e) + except: + print "Error while processing POST:" + traceback.print_exc() + _del_authinfo() + +class GlibXMLRPCServer(GlibTCPServer, SimpleXMLRPCServer.SimpleXMLRPCDispatcher): + """GlibXMLRPCServer + + Use nonblocking sockets and handle the accept via glib rather than + blocking on accept(). + """ + + def __init__(self, addr, requestHandler=GlibXMLRPCRequestHandler, logRequests=1): + self.logRequests = logRequests + + SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self) + GlibTCPServer.__init__(self, addr, requestHandler) + + def _marshaled_dispatch(self, data, dispatch_method = None): + """Dispatches an XML-RPC method from marshalled (XML) data. + + XML-RPC methods are dispatched from the marshalled (XML) data + using the _dispatch method and the result is returned as + marshalled data. For backwards compatibility, a dispatch + function can be provided as an argument (see comment in + SimpleXMLRPCRequestHandler.do_POST) but overriding the + existing method through subclassing is the prefered means + of changing method dispatch behavior. + """ + + params, method = xmlrpclib.loads(data) + + # generate response + try: + if dispatch_method is not None: + response = dispatch_method(method, params) + else: + response = self._dispatch(method, params) + # wrap response in a singleton tuple + response = (response,) + response = xmlrpclib.dumps(response, methodresponse=1) + except xmlrpclib.Fault, fault: + response = xmlrpclib.dumps(fault) + except: + print "Exception while processing request:" + traceback.print_exc() + + # report exception back to server + response = xmlrpclib.dumps( + xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)) + ) + + return response + +class GroupServer(object): + + _MAX_MSG_SIZE = 500 + + def __init__(self, address, port, data_cb): + self._address = address + self._port = port + self._data_cb = data_cb + + self._setup_listener() + + def _setup_listener(self): + # Listener socket + self._listen_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + # Set some options to make it multicast-friendly + self._listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + self._listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + except: + pass + self._listen_sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 20) + self._listen_sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) + + def start(self): + # Set some more multicast options + self._listen_sock.bind(('', self._port)) + self._listen_sock.settimeout(2) + intf = socket.gethostbyname(socket.gethostname()) + self._listen_sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(intf) + socket.inet_aton('0.0.0.0')) + self._listen_sock.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(self._address) + socket.inet_aton('0.0.0.0')) + + # Watch the listener socket for data + gobject.io_add_watch(self._listen_sock, gobject.IO_IN, self._handle_incoming_data) + + def _handle_incoming_data(self, source, condition): + if not (condition & gobject.IO_IN): + return True + msg = {} + msg['data'], (msg['addr'], msg['port']) = source.recvfrom(self._MAX_MSG_SIZE) + if self._data_cb: + self._data_cb(msg) + return True + +class GroupClient(object): + + _MAX_MSG_SIZE = 500 + + def __init__(self, address, port): + self._address = address + self._port = port + + self._send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # Make the socket multicast-aware, and set TTL. + self._send_sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20) # Change TTL (=20) to suit + + def send_msg(self, data): + self._send_sock.sendto(data, (self._address, self._port)) diff --git a/sugar/p2p/presence.py b/sugar/p2p/presence.py new file mode 100644 index 00000000..e16fc928 --- /dev/null +++ b/sugar/p2p/presence.py @@ -0,0 +1,92 @@ +# -*- tab-width: 4; indent-tabs-mode: t -*- + +import avahi, dbus, dbus.glib + +ACTION_SERVICE_NEW = 'new' +ACTION_SERVICE_REMOVED = 'removed' + +class PresenceDiscovery(object): + def __init__(self): + self.bus = dbus.SystemBus() + self.server = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER) + self._service_browsers = {} + self._service_type_browsers = {} + self._service_listeners = [] + + def add_service_listener(self, listener): + self._service_listeners.append(listener) + + def start(self): + # Always browse .local + self.browse_domain(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, "local") + db = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, self.server.DomainBrowserNew(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, "", avahi.DOMAIN_BROWSER_BROWSE, dbus.UInt32(0))), avahi.DBUS_INTERFACE_DOMAIN_BROWSER) + db.connect_to_signal('ItemNew', self.new_domain) + + def _error_handler(self, err): + print "Error resolving: %s" % err + + def resolve_service(self, interface, protocol, name, stype, domain, reply_handler, error_handler=None): + if not error_handler: + error_handler = self._error_handler + self.server.ResolveService(int(interface), int(protocol), name, stype, domain, avahi.PROTO_UNSPEC, dbus.UInt32(0), reply_handler=reply_handler, error_handler=error_handler) + + def new_service(self, interface, protocol, name, stype, domain, flags): +# print "Found service '%s' (%d) of type '%s' in domain '%s' on %i.%i." % (name, flags, stype, domain, interface, protocol) + for listener in self._service_listeners: + listener(ACTION_SERVICE_NEW, interface, protocol, name, stype, domain, flags) + + def remove_service(self, interface, protocol, name, stype, domain, flags): +# print "Service '%s' of type '%s' in domain '%s' on %i.%i disappeared." % (name, stype, domain, interface, protocol) + for listener in self._service_listeners: + listener(ACTION_SERVICE_REMOVED, interface, protocol, name, stype, domain, flags) + + def new_service_type(self, interface, protocol, stype, domain, flags): + # Are we already browsing this domain for this type? + if self._service_browsers.has_key((interface, protocol, stype, domain)): + return + +# print "Browsing for services of type '%s' in domain '%s' on %i.%i ..." % (stype, domain, interface, protocol) + + b = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, self.server.ServiceBrowserNew(interface, protocol, stype, domain, dbus.UInt32(0))), avahi.DBUS_INTERFACE_SERVICE_BROWSER) + b.connect_to_signal('ItemNew', self.new_service) + b.connect_to_signal('ItemRemove', self.remove_service) + + self._service_browsers[(interface, protocol, stype, domain)] = b + + def browse_domain(self, interface, protocol, domain): + # Are we already browsing this domain? + if self._service_type_browsers.has_key((interface, protocol, domain)): + return + +# print "Browsing domain '%s' on %i.%i ..." % (domain, interface, protocol) + + b = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, self.server.ServiceTypeBrowserNew(interface, protocol, domain, dbus.UInt32(0))), avahi.DBUS_INTERFACE_SERVICE_TYPE_BROWSER) + b.connect_to_signal('ItemNew', self.new_service_type) + + self._service_type_browsers[(interface, protocol, domain)] = b + + def new_domain(self,interface, protocol, domain, flags): + if domain != "local": + return + self.browse_domain(interface, protocol, domain) + + +class PresenceAnnounce(object): + def __init__(self): + self.bus = dbus.SystemBus() + self.server = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER) + self._hostname = None + + def register_service(self, rs_name, rs_port, rs_service, **kwargs): + g = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, self.server.EntryGroupNew()), avahi.DBUS_INTERFACE_ENTRY_GROUP) + if rs_name is None: + if self._hostname is None: + self._hostname = "%s:%s" % (self.server.GetHostName(), rs_port) + rs_name = self._hostname + + info = ["%s=%s" % (k,v) for k,v in kwargs.items()] + g.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, 0, rs_name, rs_service, + "", "", # domain, host (let the system figure it out) + dbus.UInt16(rs_port), info,) + g.Commit() + return g diff --git a/sugar/shell/Makefile.am b/sugar/shell/Makefile.am new file mode 100644 index 00000000..f7652957 --- /dev/null +++ b/sugar/shell/Makefile.am @@ -0,0 +1,21 @@ +sugardir = $(pythondir)/sugar/shell +sugar_PYTHON = \ + __init__.py \ + activity.py \ + shell.py + +# Dbus service file +servicedir = $(datadir)/dbus-1/services +service_in_files = com.redhat.Sugar.Shell.service.in +service_DATA = $(service_in_files:.service.in=.service) + +# Rule to make the service file with bindir expanded +$(service_DATA): $(service_in_files) Makefile + @sed -e "s|\@bindir\@|$(bindir)|" $< > $@ + +EXTRA_DIST = \ + $(service_in_files) \ + $(service_DATA) + +DISTCLEANFILES = \ + $(service_DATA) diff --git a/sugar/shell/__init__.py b/sugar/shell/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sugar/shell/activity.py b/sugar/shell/activity.py new file mode 100644 index 00000000..6f1f3fc1 --- /dev/null +++ b/sugar/shell/activity.py @@ -0,0 +1,178 @@ +# -*- tab-width: 4; indent-tabs-mode: t -*- + +import string + +import gc +import dbus +import dbus.service +import dbus.glib +import gobject +import pygtk +pygtk.require('2.0') +import gtk,sys + + +class Activity(dbus.service.Object): + """ Base Sugar activity object from which all other Activities should inherit """ + + def __init__(self): + pass + + def name_owner_changed(self, service_name, old_service_name, new_service_name): + #print "in name_owner_changed: svc=%s oldsvc=%s newsvc=%s"%(service_name, old_service_name, new_service_name) + if service_name == "com.redhat.Sugar.Shell" and new_service_name == "": + self.activity_on_disconnected_from_shell() + #elif service_name == "com.redhat.Sugar.Shell" and old_service_name == "": + # self.activity_on_shell_reappeared() + + def activity_connect_to_shell(self): + self.__bus = dbus.SessionBus() + + self.__bus.add_signal_receiver(self.name_owner_changed, dbus_interface = "org.freedesktop.DBus", signal_name = "NameOwnerChanged") + + self.__activity_container_object = self.__bus.get_object("com.redhat.Sugar.Shell", \ + "/com/redhat/Sugar/Shell/ActivityContainer") + self.__activity_container = dbus.Interface(self.__activity_container_object, \ + "com.redhat.Sugar.Shell.ActivityContainer") + + self.__activity_id = self.__activity_container.add_activity("") + self.__object_path = "/com/redhat/Sugar/Shell/Activities/%d"%self.__activity_id + + print "object_path = %s"%self.__object_path + + self.__activity_object = dbus.Interface(self.__bus.get_object("com.redhat.Sugar.Shell", self.__object_path), \ + "com.redhat.Sugar.Shell.ActivityHost") + self.__window_id = self.__activity_object.get_host_xembed_id() + + print "XEMBED window_id = %d"%self.__window_id + + self.__plug = gtk.Plug(self.__window_id) + + # Now let the Activity register a peer service so the Shell can poke it + self.__peer_service_name = "com.redhat.Sugar.Activity%d"%self.__activity_id + self.__peer_object_name = "/com/redhat/Sugar/Activity/%d"%self.__activity_id + self.__service = dbus.service.BusName(self.__peer_service_name, bus=self.__bus) + dbus.service.Object.__init__(self, self.__service, self.__peer_object_name) + + self.__activity_object.set_peer_service_name(self.__peer_service_name, self.__peer_object_name) + + self.activity_on_connected_to_shell() + + def activity_get_gtk_plug(self): + return self.__plug + + def activity_set_ellipsize_tab(self, ellipsize): + self.__activity_object.set_ellipsize_tab(ellipsize) + + @dbus.service.method("com.redhat.Sugar.Activity", \ + in_signature="", \ + out_signature="") + + def activity_set_can_close(self, can_close): + self.__activity_object.set_can_close(can_close) + + @dbus.service.method("com.redhat.Sugar.Activity", \ + in_signature="", \ + out_signature="") + + def activity_show_icon(self, show_icon): + self.__activity_object.set_tab_show_icon(show_icon) + + @dbus.service.method("com.redhat.Sugar.Activity", \ + in_signature="", \ + out_signature="") + + def activity_set_icon(self, pixbuf): + pixarray = [] + pixstr = pixbuf.get_pixels(); + for c in pixstr: + pixarray.append(c) + self.__activity_object.set_tab_icon(pixarray, \ + pixbuf.get_colorspace(), \ + pixbuf.get_has_alpha(), \ + pixbuf.get_bits_per_sample(), \ + pixbuf.get_width(), \ + pixbuf.get_height(), \ + pixbuf.get_rowstride()) + + @dbus.service.method("com.redhat.Sugar.Activity", \ + in_signature="", \ + out_signature="") + + def activity_set_tab_text(self, text): + self.__activity_object.set_tab_text(text) + + @dbus.service.method("com.redhat.Sugar.Activity", \ + in_signature="", \ + out_signature="") + + def activity_set_tab_icon_name(self, icon_name): + icon_theme = gtk.icon_theme_get_default() + icon_info = icon_theme.lookup_icon(icon_name, gtk.ICON_SIZE_MENU, 0) + pixbuf = icon_info.load_icon() + scaled_pixbuf = pixbuf.scale_simple(16, 16, gtk.gdk.INTERP_BILINEAR) + self.activity_set_icon(scaled_pixbuf) + + def lost_focus(self): + self.activity_on_lost_focus() + + @dbus.service.method("com.redhat.Sugar.Activity", \ + in_signature="", \ + out_signature="") + def got_focus(self): + self.activity_on_got_focus() + + + @dbus.service.method("com.redhat.Sugar.Activity", \ + in_signature="", \ + out_signature="") + def close_from_user(self): + self.activity_on_close_from_user() + + def activity_get_id(self): + return self.__activity_id + + + def __shutdown_reply_cb(self): + print "in __reply_cb" + + self.__plug.destroy() + self.__plug = None + + self.__bus = None + self.__activity_container_object = None + self.__activity_container = None + self.__activity_object = None + self.__service = None + + self.__bus.remove_signal_receiver(self.name_owner_changed, dbus_interface = "org.freedesktop.DBus", signal_name = "NameOwnerChanged") + + self.activity_on_disconnected_from_shell() + + + del self + + + + def __shutdown_error_cb(self, error): + print "in __error_cb" + + def activity_shutdown(self): + self.__activity_object.shutdown(reply_handler = self.__shutdown_reply_cb, error_handler = self.__shutdown_error_cb) + + # pure virtual methods + + def activity_on_connected_to_shell(self): + print "act %d: you need to override activity_on_connected_to_shell"%self.activity_get_id() + + def activity_on_disconnected_from_shell(self): + print "act %d: you need to override activity_on_disconnected_from_shell"%self.activity_get_id() + + def activity_on_close_from_user(self): + print "act %d: you need to override activity_on_close_from_user"%self.activity_get_id() + + def activity_on_lost_focus(self): + print "act %d: you need to override activity_on_lost_focus"%self.activity_get_id() + + def activity_on_got_focus(self): + print "act %d: you need to override activity_on_got_focus"%self.activity_get_id() diff --git a/sugar/shell/com.redhat.Sugar.Shell.service.in b/sugar/shell/com.redhat.Sugar.Shell.service.in new file mode 100644 index 00000000..2a069a17 --- /dev/null +++ b/sugar/shell/com.redhat.Sugar.Shell.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=com.redhat.Sugar.Shell +Exec=@bindir@/sugar shell diff --git a/sugar/shell/example-activity/example-activity.py b/sugar/shell/example-activity/example-activity.py new file mode 100755 index 00000000..05ce55af --- /dev/null +++ b/sugar/shell/example-activity/example-activity.py @@ -0,0 +1,82 @@ +#!/usr/bin/python +# -*- tab-width: 4; indent-tabs-mode: t -*- + +import string + +import gc +import dbus +import dbus.service +import dbus.glib +import gobject +import pygtk +pygtk.require('2.0') +import gtk,sys + +import activity + +def my_exit(): + sys.exit(0) + +def deferred_exit(): + gobject.timeout_add(0, my_exit) + +################################################################################ + +class ExampleActivity(activity.Activity): + + def __init__(self, name): + self.name = name + + def entry_changed(self, entry): + self.activity_set_tab_text(entry.get_text()) + + def activity_on_connected_to_shell(self): + print "act %d: in activity_on_connected_to_shell"%self.activity_get_id() + + self.activity_set_tab_text(self.name) + + plug = self.activity_get_gtk_plug() + self.entry = gtk.Entry() + self.entry.set_text(self.name) + self.entry.connect("changed", self.entry_changed) + plug.add(self.entry) + plug.show_all() + + icon_theme = gtk.icon_theme_get_default() + pixbuf = icon_theme.load_icon("gnome-dev-cdrom", gtk.ICON_SIZE_MENU, gtk.ICON_LOOKUP_USE_BUILTIN) + self.activity_set_icon(pixbuf) + self.activity_show_icon(True) + + def activity_on_disconnected_from_shell(self): + print "act %d: in activity_on_disconnected_from_shell"%self.activity_get_id() + print "act %d: Shell disappeared..."%self.activity_get_id() + + gc.collect() + + def activity_on_close_from_user(self): + print "act %d: in activity_on_close_from_user"%self.activity_get_id() + self.activity_shutdown() + + def activity_on_lost_focus(self): + print "act %d: in activity_on_lost_focus"%self.activity_get_id() + + def activity_on_got_focus(self): + print "act %d: in activity_on_got_focus"%self.activity_get_id() + + def __del__(self): + print "in __del__ for ExampleActivity" + + +if len(sys.argv) != 2: + print "usage: example-activity.py " + sys.exit(1) + +gc.set_debug(gc.DEBUG_LEAK) + +example_activity = ExampleActivity(sys.argv[1]) +example_activity.activity_connect_to_shell() +example_activity = None + +gtk.main() + + diff --git a/sugar/shell/shell.py b/sugar/shell/shell.py new file mode 100755 index 00000000..d5423079 --- /dev/null +++ b/sugar/shell/shell.py @@ -0,0 +1,305 @@ +#!/usr/bin/python +# -*- tab-width: 4; indent-tabs-mode: t -*- + +import string + +import dbus +import dbus.service +import dbus.glib +import gobject +import pygtk +pygtk.require('2.0') +import gtk +import pango + +activity_counter = 0 + +class ActivityHost(dbus.service.Object): + + def __init__(self, activity_container, activity_name): + global activity_counter + + self.activity_name = activity_name + self.ellipsize_tab = False + + self.activity_container = activity_container + + self.activity_id = activity_counter + activity_counter += 1 + + self.dbus_object_name = "/com/redhat/Sugar/Shell/Activities/%d"%self.activity_id + #print "object name = %s"%self.dbus_object_name + + dbus.service.Object.__init__(self, activity_container.service, self.dbus_object_name) + self.socket = gtk.Socket() + self.socket.set_data("sugar-activity", self) + self.socket.show() + + hbox = gtk.HBox(False, 4); + + self.tab_activity_image = gtk.Image() + self.tab_activity_image.set_from_stock(gtk.STOCK_CONVERT, gtk.ICON_SIZE_MENU) + hbox.pack_start(self.tab_activity_image) + #self.tab_activity_image.show() + + self.label_hbox = gtk.HBox(False, 4); + self.label_hbox.connect("style-set", self.__tab_label_style_set_cb) + hbox.pack_start(self.label_hbox) + + self.tab_label = gtk.Label(self.activity_name) + self.tab_label.set_single_line_mode(True) + self.tab_label.set_alignment(0.0, 0.5) + self.tab_label.set_padding(0, 0) + self.tab_label.show() + + close_image = gtk.Image() + close_image.set_from_stock (gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU) + close_image.show() + + self.tab_close_button = gtk.Button() + rcstyle = gtk.RcStyle(); + rcstyle.xthickness = rcstyle.ythickness = 0; + self.tab_close_button.modify_style (rcstyle); + self.tab_close_button.add(close_image) + self.tab_close_button.set_relief(gtk.RELIEF_NONE) + self.tab_close_button.set_focus_on_click(False) + self.tab_close_button.connect("clicked", self.tab_close_button_clicked) + + self.label_hbox.pack_start(self.tab_label) + self.label_hbox.pack_start(self.tab_close_button, False, False, 0) + self.label_hbox.show() + + hbox.show() + + notebook = self.activity_container.notebook + index = notebook.append_page(self.socket, hbox) + notebook.set_current_page(index) + + def __close_button_clicked_reply_cb(self): + pass + + def __close_button_clicked_error_cb(self, error): + pass + + def tab_close_button_clicked(self, button): + self.peer_service.close_from_user(reply_handler = self.__close_button_clicked_reply_cb, \ + error_handler = self.__close_button_clicked_error_cb) + + @dbus.service.method("com.redhat.Sugar.Shell.ActivityHost", \ + in_signature="", \ + out_signature="t") + def get_host_xembed_id(self): + window_id = self.socket.get_id() + #print "window_id = %d"%window_id + return window_id + + @dbus.service.method("com.redhat.Sugar.Shell.ActivityHost", \ + in_signature="ss", \ + out_signature="") + def set_peer_service_name(self, peer_service_name, peer_object_name): + #print "peer_service_name = %s, peer_object_name = %s"%(peer_service_name, peer_object_name) + self.__peer_service_name = peer_service_name + self.__peer_object_name = peer_object_name + self.peer_service = dbus.Interface(self.activity_container.bus.get_object( \ + self.__peer_service_name, self.__peer_object_name), \ + "com.redhat.Sugar.Activity") + + @dbus.service.method("com.redhat.Sugar.Shell.ActivityHost", \ + in_signature="b", \ + out_signature="") + def set_ellipsize_tab(self, ellipsize): + self.ellipsize_tab = True + self.update_tab_size() + + @dbus.service.method("com.redhat.Sugar.Shell.ActivityHost", \ + in_signature="b", \ + out_signature="") + def set_can_close(self, can_close): + if can_close: + self.tab_close_button.show() + else: + self.tab_close_button.hide() + + @dbus.service.method("com.redhat.Sugar.Shell.ActivityHost", \ + in_signature="b", \ + out_signature="") + def set_tab_show_icon(self, show_icon): + if show_icon: + self.tab_activity_image.show() + else: + self.tab_activity_image.hide() + + @dbus.service.method("com.redhat.Sugar.Shell.ActivityHost", \ + in_signature="s", \ + out_signature="") + def set_tab_text(self, text): + self.tab_label.set_text(text) + + @dbus.service.method("com.redhat.Sugar.Shell.ActivityHost", \ + in_signature="ayibiiii", \ + out_signature="") + def set_tab_icon(self, data, colorspace, has_alpha, bits_per_sample, width, height, rowstride): + #print "width=%d, height=%d"%(width, height) + #print " data = ", data + pixstr = "" + for c in data: + pixstr += chr(c) + + pixbuf = gtk.gdk.pixbuf_new_from_data(pixstr, colorspace, has_alpha, bits_per_sample, width, height, rowstride) + #print pixbuf + self.tab_activity_image.set_from_pixbuf(pixbuf) + + @dbus.service.method("com.redhat.Sugar.Shell.ActivityHost", \ + in_signature="", \ + out_signature="") + def shutdown(self): + #print "shutdown" + for owner, activity in self.activity_container.activities[:]: + if activity == self: + self.activity_container.activities.remove((owner, activity)) + + for i in range(self.activity_container.notebook.get_n_pages()): + child = self.activity_container.notebook.get_nth_page(i) + if child == self.socket: + #print "found child" + self.activity_container.notebook.remove_page(i) + break + + del self + + def get_host_activity_id(self): + return self.activity_id + + def get_object_path(self): + return self.dbus_object_name + + def update_tab_size(self): + if self.ellipsize_tab: + self.tab_label.set_ellipsize(pango.ELLIPSIZE_END) + + context = self.label_hbox.get_pango_context() + font_desc = self.label_hbox.style.font_desc + metrics = context.get_metrics(font_desc, context.get_language()) + char_width = metrics.get_approximate_digit_width() + [w, h] = self.__get_close_icon_size() + tab_width = 15 * pango.PIXELS(char_width) + 2 * w + self.label_hbox.set_size_request(tab_width, -1); + else: + self.tab_label.set_ellipsize(pango.ELLIPSIZE_NONE) + self.label_hbox.set_size_request(-1, -1) + + def __get_close_icon_size(self): + settings = self.label_hbox.get_settings() + return gtk.icon_size_lookup_for_settings(settings, gtk.ICON_SIZE_MENU) + + def __tab_label_style_set_cb(self, widget, previous_style): + [w, h] = self.__get_close_icon_size() + self.tab_close_button.set_size_request (w + 5, h + 2) + self.update_tab_size() + +class ActivityContainer(dbus.service.Object): + + def __init__(self, service, bus): + + self.activities = [] + + self.bus = bus + self.service = service + + dbus.service.Object.__init__(self, self.service, "/com/redhat/Sugar/Shell/ActivityContainer") + bus.add_signal_receiver(self.name_owner_changed, dbus_interface = "org.freedesktop.DBus", signal_name = "NameOwnerChanged") + + self.window = gtk.Window() + self.window.set_title("OLPC Sugar") + self.window.resize(640, 480) + self.window.set_geometry_hints(min_width = 640, max_width = 640, min_height = 480, max_height = 480) + self.notebook = gtk.Notebook() + + #tab_label = gtk.Label("My Laptop") + #empty_label = gtk.Label("This activity could launch other activities / be a help page") + #empty_label.show() + #self.notebook.append_page(empty_label, tab_label) + + self.notebook.show() + self.notebook.connect("switch-page", self.notebook_tab_changed) + self.window.add(self.notebook) + + self.window.connect("destroy", lambda w: gtk.main_quit()) + self.window.show() + + self.current_activity = None + + + def __focus_reply_cb(self): + pass + + def __focus_error_cb(self, error): + pass + + + def notebook_tab_changed(self, notebook, page, page_number): + #print "in notebook_tab_changed" + #print notebook.get_nth_page(page_number) + new_activity = notebook.get_nth_page(page_number).get_data("sugar-activity") + #print " Current activity: ", self.current_activity + #print " New activity: ", new_activity + + if self.current_activity != None: + if self.has_activity(self.current_activity): + self.current_activity.peer_service.lost_focus(reply_handler = self.__focus_reply_cb, error_handler = self.__focus_error_cb) + + self.current_activity = new_activity + + if self.current_activity != None: + if self.has_activity(self.current_activity): + self.current_activity.peer_service.got_focus(reply_handler = self.__focus_reply_cb, error_handler = self.__focus_error_cb) + + + def has_activity(self, activity_to_check_for): + for owner, activity in self.activities[:]: + if activity_to_check_for == activity: + return True + return False + + + def name_owner_changed(self, service_name, old_service_name, new_service_name): + #print "in name_owner_changed: svc=%s oldsvc=%s newsvc=%s"%(service_name, old_service_name, new_service_name) + for owner, activity in self.activities[:]: + if owner == old_service_name: + self.activities.remove((owner, activity)) + #self.__print_activities() + + + @dbus.service.method("com.redhat.Sugar.Shell.ActivityContainer", \ + in_signature="s", \ + out_signature="i", \ + sender_keyword="sender") + def add_activity(self, activity_name, sender): + #print "hello world, activity_name = '%s', sender = '%s'"%(activity_name, sender) + activity = ActivityHost(self, activity_name) + self.activities.append((sender, activity)) + + #self.__print_activities() + return activity.get_host_activity_id() + + def __print_activities(self): + print "__print_activities: %d activities registered"%len(self.activities) + i = 0 + for owner, activity in self.activities: + print " %d: owner=%s activity_object_name=%s"%(i, owner, activity.dbus_object_name) + i += 1 + + +def main(): + session_bus = dbus.SessionBus() + service = dbus.service.BusName("com.redhat.Sugar.Shell", bus=session_bus) + + activityContainer = ActivityContainer(service, session_bus) + + try: + gtk.main() + except KeyboardInterrupt: + pass + +if __name__=="__main__": + main() diff --git a/sugar/sugar b/sugar/sugar new file mode 100755 index 00000000..960971c4 --- /dev/null +++ b/sugar/sugar @@ -0,0 +1,28 @@ +#!/usr/bin/python + +import sys +import os + +if len(sys.argv) == 1: + # FIXME Start a session + + # We are lucky and this + # currently behave as we want. + # The chat depends on the + # web browser, so both activities + # are spanned. But obviously we + # need something better. + + import sugar.chat + sugar.chat.main() +elif sys.argv[1] == 'shell': + import sugar.shell + sugar.shell.main() +elif sys.argv[1] == 'chat': + import sugar.chat + sugar.chat.main() +elif sys.argv[1] == 'browser': + import sugar.browser + sugar.browser.main() +else: + print "Unknown activity"