diff options
-rwxr-xr-x | activity/activity.info | 2 | ||||
-rw-r--r-- | browser.dtd | 21 | ||||
-rw-r--r-- | icons/add-link.svg | 13 | ||||
-rw-r--r-- | icons/buddy-link.svg | 23 | ||||
-rw-r--r-- | linkbutton.py | 89 | ||||
-rw-r--r-- | linktoolbar.py | 94 | ||||
-rw-r--r-- | messenger.py | 126 | ||||
-rw-r--r-- | model.py | 178 | ||||
-rw-r--r-- | po/Web.pot | 30 | ||||
-rw-r--r-- | sessionhistory.py | 9 | ||||
-rw-r--r-- | tubeconn.py | 107 | ||||
-rwxr-xr-x | webactivity.py | 297 | ||||
-rwxr-xr-x | webtoolbar.py | 5 |
13 files changed, 957 insertions, 37 deletions
diff --git a/activity/activity.info b/activity/activity.info index 3959bda..85d9e84 100755 --- a/activity/activity.info +++ b/activity/activity.info @@ -1,6 +1,6 @@ [Activity] name = Browse -activity_version = 38 +activity_version = 39 service_name = org.laptop.WebActivity icon = activity-web class = webactivity.WebActivity diff --git a/browser.dtd b/browser.dtd new file mode 100644 index 0000000..ae2da59 --- /dev/null +++ b/browser.dtd @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<!ELEMENT browser (#PCDATA|link|session)*> +<!ATTLIST browser + name CDATA #REQUIRED +> + +<!ELEMENT session (#PCDATA)* > +<!ATTLIST session + data CDATA #IMPLIED +> +<!ELEMENT link (#PCDATA)* > +<!ATTLIST link + hash CDATA #REQUIRED + url CDATA #IMPLIED + title CDATA #IMPLIED + thumb CDATA #IMPLIED + owner CDATA #IMPLIED + color CDATA #IMPLIED + deleted CDATA #IMPLIED +> diff --git a/icons/add-link.svg b/icons/add-link.svg new file mode 100644 index 0000000..b84d3ef --- /dev/null +++ b/icons/add-link.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 12.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY ns_svg "http://www.w3.org/2000/svg">
+ <!ENTITY ns_xlink "http://www.w3.org/1999/xlink">
+ <!ENTITY stroke_color "#020202"> + <!ENTITY fill_color "#B2B2B2"> +]> +<svg version="1.1" id="Icon" xmlns="&ns_svg;" xmlns:xlink="&ns_xlink;" width="28.479" height="27.086"
+ viewBox="0 0 28.479 27.086" overflow="visible" enable-background="new 0 0 28.479 27.086" xml:space="preserve">
+<polygon fill="&fill_color;" stroke="&stroke_color;" stroke-width="2.25" stroke-linecap="round" points="14.24,2.542 17.893,9.946
+ 26.062,11.132 20.151,16.894 21.546,25.03 14.24,21.188 6.933,25.03 8.329,16.894 2.417,11.132 10.586,9.946 "/>
+</svg>
diff --git a/icons/buddy-link.svg b/icons/buddy-link.svg new file mode 100644 index 0000000..7e79838 --- /dev/null +++ b/icons/buddy-link.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ + <!ENTITY ns_svg "http://www.w3.org/2000/svg"> + <!ENTITY ns_xlink "http://www.w3.org/1999/xlink"> + <!ENTITY stroke_color "#020202"> + <!ENTITY fill_color "#0000ff"> + <!ENTITY b_fill "#B2B2B2"> + <!ENTITY b_stroke "#000000"> +]> + +<svg version="1.1" id="Icon" xmlns="&ns_svg;" xmlns:xlink="&ns_xlink;" width="64" height="64" + viewBox="0 0 64 64" overflow="visible" enable-background="new 0 0 64 64" xml:space="preserve"> + + <rect fill="&b_fill;" stroke="&b_stroke;" stroke-width="3px" x="3" y="2" width="60" height="60" rx="8"/> + + <path + d="M 38.404455,39.235864 L 48.504455,49.335864 C 49.256455,50.085864 49.721455,51.120864 49.721455,52.267864 C 49.721455,54.554864 47.866455,56.410864 45.575455,56.410864 C 44.430455,56.410864 43.397455,55.947864 42.643455,55.199864 L 32.543455,45.097864 L 22.443455,55.197864 C 21.693455,55.947864 20.656455,56.408864 19.510455,56.408864 C 17.225455,56.408864 15.368455,54.554864 15.368455,52.267864 C 15.368455,51.121864 15.833455,50.083864 16.580455,49.333864 L 26.683455,39.233864 L 16.580455,29.131864 C 15.833455,28.382864 15.368455,27.346864 15.368455,26.201864 C 15.368455,23.912864 17.221455,22.056864 19.513455,22.056864 C 20.656455,22.056864 21.693455,22.521864 22.443455,23.270864 L 32.542455,33.371864 L 42.642455,23.269864 C 43.396455,22.520864 44.429455,22.055864 45.576455,22.055864 C 47.865455,22.055864 49.722455,23.911864 49.722455,26.200864 C 49.722455,27.345864 49.255455,28.379864 48.505455,29.130864 L 38.404455,39.235864 z " + fill="&fill_color;" stroke="&stroke_color;" stroke-width="3.5" /> + + <circle cx="18.924999" cy="9.8719997" r="8.1219997" transform="translate(13.617455,5.111864)" + fill="&fill_color;" stroke="&stroke_color;" stroke-width="3.5" /> + +</svg> diff --git a/linkbutton.py b/linkbutton.py new file mode 100644 index 0000000..2b628bb --- /dev/null +++ b/linkbutton.py @@ -0,0 +1,89 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import gtk +import os + +import rsvg +import re + +from sugar.graphics.palette import Palette, WidgetInvoker +from sugar.graphics import style + + +class LinkButton(gtk.RadioToolButton): + def __init__(self, buffer, color, pos, group=None): + gtk.RadioToolButton.__init__(self, group=group) + self._palette = None + self.set_image(buffer, color.split(',')[1], color.split(',')[0]) + self.pos = pos + + def set_image(self, buffer, fill='#0000ff', stroke='#4d4c4f'): + img = gtk.Image() + loader = gtk.gdk.PixbufLoader() + loader.write(buffer) + loader.close() + pixbuf = loader.get_pixbuf() + del loader + + xo_buddy = os.path.join(os.path.dirname(__file__), "icons/buddy-link.svg") + pixbuf_xo = self._read_xo_icon(xo_buddy, fill, stroke) + + width = pixbuf_xo.get_width() + height = pixbuf_xo.get_height() + + dest_x = style.zoom(105) + dest_y = style.zoom(65) + w = width*0.7 + h = height*0.7 + scale_x = 0.7 + scale_y = 0.7 + + pixbuf_xo.composite(pixbuf, dest_x, dest_y, w, h, dest_x, dest_y, scale_x, scale_y, gtk.gdk.INTERP_BILINEAR, 255) + + img.set_from_pixbuf(pixbuf) + self.set_icon_widget(img) + img.show() + + def _read_xo_icon(self, filename, fill_color, stroke_color): + icon_file = open(filename, 'r') + data = icon_file.read() + icon_file.close() + + if fill_color: + entity = '<!ENTITY fill_color "%s">' % fill_color + data = re.sub('<!ENTITY fill_color .*>', entity, data) + + if stroke_color: + entity = '<!ENTITY stroke_color "%s">' % stroke_color + data = re.sub('<!ENTITY stroke_color .*>', entity, data) + + data_size = len(data) + return rsvg.Handle(data=data).get_pixbuf() + + def get_palette(self): + return self._palette + + def set_palette(self, palette): + self._palette = palette + self._palette.props.invoker = WidgetInvoker(self.child) + + def set_tooltip(self, text): + self._palette = Palette(text) + self._palette.props.invoker = WidgetInvoker(self.child) + + palette = property(get_palette, set_palette) diff --git a/linktoolbar.py b/linktoolbar.py new file mode 100644 index 0000000..35ad3f0 --- /dev/null +++ b/linktoolbar.py @@ -0,0 +1,94 @@ +# Copyright (C) 2007, One Laptop Per Child +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import logging + +import gobject +import gtk + +from gettext import gettext as _ + +from linkbutton import LinkButton +from sugar.graphics.palette import Palette + +_logger = logging.getLogger('linktoolbar') + +class LinkToolbar(gtk.Toolbar): + __gtype_name__ = 'LinkToolbar' + + __gsignals__ = { + 'link-selected': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([str])), + 'link-rm': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([int])) + } + + def __init__(self): + gtk.Toolbar.__init__(self) + + def _add_link(self, url, buffer, color, title, owner, pos): + + if self.get_children(): + group = self.get_children()[0] + else: + group = None + + info = _('title: ') + title +'\n' + _('url: ') + url + '\n' + _('owner: ') + owner + palette = Palette(info) + palette.props.position = Palette.TOP + + link = LinkButton(buffer, color, pos, group) + link.set_palette(palette) + link.connect('clicked', self._link_clicked_cb, url) + self.insert(link, 0) + link.show() + + menu_item = gtk.MenuItem(_('remove')) + menu_item.connect('activate', self._link_rm_palette_cb, link) + palette.menu.append(menu_item) + menu_item.show() + + #link.props.active = True + + if len(self.get_children()) > 0: + self.show() + + def _link_clicked_cb(self, link, url): + if link.get_active(): + _logger.debug('link clicked=%s' %url) + self.emit('link-selected', url) + + def _rm_link(self): + childs = self.get_children() + for child in childs: + if child.get_active(): + index = child.pos + self.remove(child) + # self.get_children()[0].props.active = True + if len(self.get_children()) is 0: + self.hide() + return index + + def _link_rm_palette_cb(self, widget, link): + self.emit('link-rm', link.pos) + self.remove(link) + # self.get_children()[0].props.active = True + if len(self.get_children()) is 0: + self.hide() + diff --git a/messenger.py b/messenger.py new file mode 100644 index 0000000..489be0e --- /dev/null +++ b/messenger.py @@ -0,0 +1,126 @@ +# +# Copyright (C) 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# + +import logging +import os +import dbus +from dbus.gobject_service import ExportedGObject +import base64 +import sha + +SERVICE = "org.laptop.WebActivity" +IFACE = SERVICE +PATH = "/org/laptop/WebActivity" + +_logger = logging.getLogger('messenger') + +class Messenger(ExportedGObject): + def __init__(self, tube, is_initiator, model, linkbar, owner): + ExportedGObject.__init__(self, tube, PATH) + self.tube = tube + self.is_initiator = is_initiator + self.members = [] + self.entered = False + self.linkbar = linkbar + self.model = model + self.owner = owner + self.tube.watch_participants(self.participant_change_cb) + + def participant_change_cb(self, added, removed): + _logger.debug('Participants change add=%s rem=%s' %(added, removed)) + for handle, bus_name in added: + _logger.debug('Add member handle=%s bus_name=%s' %(str(handle), str(bus_name))) + self.members.append(bus_name) + + for handle, bus_name in removed: + _logger.debug('Remove member handle=%s bus_name=%s' %(str(handle), str(bus_name))) + try: + self.members.remove(bus_name) + except ValueError: + # already absent + pass + + if not self.entered: + self.tube.add_signal_receiver(self._add_link_receiver, '_add_link', IFACE, path=PATH, sender_keyword='sender', + byte_arrays=True) + if self.is_initiator: + _logger.debug('Initialising a new shared browser, I am %s .'%self.tube.get_unique_name()) + else: + # sync with other members + self.bus_name = self.tube.get_unique_name() + _logger.debug('Joined I am %s .'%self.bus_name) + for member in self.members: + if member != self.bus_name: + _logger.debug('Get info from %s' %member) + self.tube.get_object(member, PATH).sync_with_members(self.model.get_links_ids(), dbus_interface=IFACE, reply_handler=self.reply_sync, error_handler=lambda e:self.error_sync(e, 'transfering file')) + + self.entered = True + + def reply_sync(self, a_ids): + a_ids.pop() + for link in self.model.links: + if link['hash'] not in a_ids: + if link['deleted'] == 0: + self.tube.get_object(sender, PATH).send_link(link['hash'], link['url'], link['title'], link['color'], + link['owner'], base64.b64encode(link['thumb'])) + + def error_sync(self, e, when): + _logger.error('Error %s: %s'%(when, e)) + + @dbus.service.method(dbus_interface=IFACE, in_signature='as', out_signature='as', sender_keyword='sender') + def sync_with_members(self, b_ids, sender=None): + '''Sync with members ''' + b_ids.pop() + # links the caller wants from me + for link in self.model.links: + if link['hash'] not in b_ids: + if link['deleted'] == 0: + self.tube.get_object(sender, PATH).send_link(link['hash'], link['url'], link['title'], link['color'], + link['owner'], base64.b64encode(link['thumb'])) + a_ids = self.model.get_links_ids() + a_ids.append('') + # links I want from the caller + return a_ids + + @dbus.service.method(dbus_interface=IFACE, in_signature='ssssss', out_signature='') + def send_link(self, id, url, title, color, owner, buffer): + '''Send link''' + _logger.debug('Received data for link.') + a_ids = self.model.get_links_ids() + if id not in a_ids: + thumb = base64.b64decode(buffer) + self.model.links.append( {'hash':sha.new(url).hexdigest(), 'url':url, 'title':title, 'thumb':thumb, + 'owner':owner, 'color':color, 'deleted':0} ) + self.linkbar._add_link(url, thumb, color, title, owner, len(self.model.links)-1) + + @dbus.service.signal(IFACE, signature='sssss') + def _add_link(self, url, title, color, owner, thumb): + '''Signal to send the link information (add)''' + _logger.debug('Add Link: %s '%url) + + def _add_link_receiver(self, url, title, color, owner, thumb, sender=None): + '''Member sent a link''' + handle = self.tube.bus_name_to_handle[sender] + if self.tube.self_handle != handle: + buffer = base64.b64decode(thumb) + + self.model.links.append( {'hash':sha.new(url).hexdigest(), 'url':url, 'title':title, 'thumb':buffer, + 'owner':owner, 'color':color, 'deleted':0} ) + self.linkbar._add_link(url, buffer, color, title, owner, len(self.model.links)-1) + _logger.debug('Added link: %s to linkbar.'%(url)) + diff --git a/model.py b/model.py new file mode 100644 index 0000000..284cb7f --- /dev/null +++ b/model.py @@ -0,0 +1,178 @@ +# +# Copyright (C) 2006, 2007, One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# + +import libxml2 +import os +import logging +import base64 + + +_logger = logging.getLogger('model') + + +class Model(object): + ''' The model of the activity. Contains methods to read and write + the configuration for a browser session to and from xml. + ''' + + def __init__(self, dtdpath): + self.links = [] + self.data = {} + self.dtdpath = dtdpath + self.data['name'] = 'first' + self.session_data = '' + + try: + self.dtd = libxml2.parseDTD(None, os.path.join(self.dtdpath, 'browser.dtd')) + except libxml2.parserError, e: + _logger.error('Init: no browser.dtd found ' +str(e)) + self.dtd = None + self.ctxt = libxml2.newValidCtxt() + + def read(self, filepath): + ''' reads the configuration from an xml file ''' + + try: + doc = libxml2.parseFile(filepath) + if doc.validateDtd(self.ctxt, self.dtd): + + # get the requested nodes + xpa = doc.xpathNewContext() + res = xpa.xpathEval("//*") + + # write their content to the data structure + for elem in res: + attributes = elem.get_properties() + if( elem.name == 'link' ): + for attribute in attributes: + if(attribute.name == 'hash'): + hash = attribute.content + elif(attribute.name == 'url'): + url = attribute.content + elif(attribute.name == 'title'): + title = attribute.content + elif(attribute.name == 'thumb'): + thumb = base64.b64decode(attribute.content) + elif(attribute.name == 'owner'): + owner = attribute.content + elif(attribute.name == 'color'): + color = attribute.content + elif(attribute.name == 'deleted'): + deleted = int(attribute.content) + + self.links.append( {'hash':hash, 'url':url, 'title':title, 'thumb':thumb, + 'owner':owner, 'color':color, 'deleted':deleted} ) + + elif( elem.name == 'session' ): + for attribute in attributes: + if(attribute.name == 'data'): + self.session_data = attribute.content + + elif( elem.name == 'browser' ): + for attribute in attributes: + if(attribute.name == 'name'): + self.data['name'] = attribute.content + + xpa.xpathFreeContext() + else: + _logger.error('Read: Error in validation of the file') + doc.freeDoc() + return 1 + doc.freeDoc() + return 0 + except libxml2.parserError, e: + _logger.error('Read: Error parsing file ' +str(e)) + return 2 + + + def write(self, filepath): + ''' writes the configuration to an xml file ''' + doc = libxml2.newDoc("1.0") + root = doc.newChild(None, "browser", None) + + if(self.data.get('name', None) != None): + root.setProp("name", self.data['name']) + else: + _logger.error('Write: No name is specified. Can not write session.') + return 1 + + elem = root.newChild(None, "session", None) + elem.setProp("data", self.session_data) + + for link in self.links: + elem = root.newChild(None, "link", None) + elem.setProp("hash", link['hash']) + elem.setProp("url", link['url']) + elem.setProp("title", link['title']) + elem.setProp("thumb", base64.b64encode(link['thumb'])) + elem.setProp("owner", link['owner']) + elem.setProp("color", link['color']) + elem.setProp("deleted", str(link['deleted'])) + + if doc.validateDtd(self.ctxt, self.dtd): + doc.saveFormatFile(filepath, 1) + else: + _logger.error('Write: Error in validation of the file') + doc.freeDoc() + return 2 + doc.freeDoc() + return 0 + + def get_links_ids(self): + ids = [] + for link in self.links: + ids.append(link['hash']) + ids.append('') + return ids + + +if __name__ == '__main__': + model = Model(os.path.dirname(__file__)) + + filepath = 'sports.png' + + target = os.open(filepath, os.O_RDONLY) + filelen = os.stat(filepath).st_size + data = os.read(target, filelen) + os.close(target) + + ''' + import sha + url = 'www.sport.de' + title = 'sports' + hash = sha.new(url) + model.links.append({'hash':hash.hexdigest(), 'url':url, 'title':title}) + + url = 'www.jazz.de' + title = 'more on jazz' + hash = sha.new(url) + model.links.append({'hash':hash.hexdigest(), 'url':url, 'title':title}) + + url = 'www.taz.de' + title = 'die zeitung' + hash = sha.new(url) + model.links.append({'hash':hash.hexdigest(), 'url':url, 'title':title}) + + model.write('/tmp/test.bwr') + ''' + + model.read('/tmp/test.bwr') + + model.links.remove(model.links[1]) + + print model.links @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2007-08-20 10:55-0400\n" +"POT-Creation-Date: 2007-08-19 20:16+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -16,10 +16,6 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: activity/activity.info:2 webactivity.py:64 -msgid "Browse" -msgstr "" - #: downloadmanager.py:114 #, python-format msgid "" @@ -41,6 +37,30 @@ msgid "" "%s." msgstr "" +#: linktoolbar.py:52 +msgid "title: " +msgstr "" + +#: linktoolbar.py:52 +msgid "url: " +msgstr "" + +#: linktoolbar.py:52 +msgid "owner: " +msgstr "" + +#: linktoolbar.py:62 +msgid "remove" +msgstr "" + +#: webactivity.py:79 +msgid "Browse" +msgstr "" + +#: webactivity.py:104 webactivity.py:105 +msgid "blank" +msgstr "" + #: webtoolbar.py:41 msgid "Back" msgstr "" diff --git a/sessionhistory.py b/sessionhistory.py index 5d058c7..f64a3fb 100644 --- a/sessionhistory.py +++ b/sessionhistory.py @@ -25,7 +25,9 @@ class HistoryListener(gobject.GObject): __gsignals__ = { 'session-history-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, - ([int])) + ([int])), + 'session-link-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str])) } def __init__(self, browser): @@ -39,21 +41,25 @@ class HistoryListener(gobject.GObject): def OnHistoryGoBack(self, back_uri): logging.debug("OnHistoryGoBack: %s" % back_uri.spec) + self.emit('session-link-changed', back_uri.spec) self.emit('session-history-changed', self._session_history.index - 1) return True def OnHistoryGoForward(self, forward_uri): logging.debug("OnHistoryGoForward: %s" % forward_uri.spec) + self.emit('session-link-changed', forward_uri.spec) self.emit('session-history-changed', self._session_history.index + 1) return True def OnHistoryGotoIndex(self, index, goto_uri): logging.debug("OnHistoryGotoIndex: %i %s" % (index, goto_uri.spec)) + self.emit('session-link-changed', goto_uri.spec) self.emit('session-history-changed', index) return True def OnHistoryNewEntry(self, new_uri): logging.debug("OnHistoryNewEntry: %s" % new_uri.spec) + self.emit('session-link-changed', new_uri.spec) self.emit('session-history-changed', self._session_history.index + 1) def OnHistoryPurge(self, num_entries): @@ -62,6 +68,7 @@ class HistoryListener(gobject.GObject): return True def OnHistoryReload(self, reload_uri, reload_flags): + self.emit('session-link-changed', reload_uri.spec) logging.debug("OnHistoryReload: %s" % reload_uri.spec) return True diff --git a/tubeconn.py b/tubeconn.py new file mode 100644 index 0000000..d1c1403 --- /dev/null +++ b/tubeconn.py @@ -0,0 +1,107 @@ +# This should eventually land in telepathy-python, so has the same license: + +# Copyright (C) 2007 Collabora Ltd. <http://www.collabora.co.uk/> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +__all__ = ('TubeConnection',) +__docformat__ = 'reStructuredText' + + +import logging + +from dbus.connection import Connection + + +logger = logging.getLogger('telepathy.tubeconn') + + +class TubeConnection(Connection): + + def __new__(cls, conn, tubes_iface, tube_id, address=None, + group_iface=None, mainloop=None): + if address is None: + address = tubes_iface.GetDBusServerAddress(tube_id) + self = super(TubeConnection, cls).__new__(cls, address, + mainloop=mainloop) + + self._tubes_iface = tubes_iface + self.tube_id = tube_id + self.participants = {} + self.bus_name_to_handle = {} + self._mapping_watches = [] + + if group_iface is None: + method = conn.GetSelfHandle + else: + method = group_iface.GetSelfHandle + method(reply_handler=self._on_get_self_handle_reply, + error_handler=self._on_get_self_handle_error) + + return self + + def _on_get_self_handle_reply(self, handle): + self.self_handle = handle + match = self._tubes_iface.connect_to_signal('DBusNamesChanged', + self._on_dbus_names_changed) + self._tubes_iface.GetDBusNames(self.tube_id, + reply_handler=self._on_get_dbus_names_reply, + error_handler=self._on_get_dbus_names_error) + self._dbus_names_changed_match = match + + def _on_get_self_handle_error(self, e): + logging.basicConfig() + logger.error('GetSelfHandle failed: %s', e) + + def close(self): + self._dbus_names_changed_match.remove() + self._on_dbus_names_changed(self.tube_id, (), self.participants.keys()) + super(TubeConnection, self).close() + + def _on_get_dbus_names_reply(self, names): + self._on_dbus_names_changed(self.tube_id, names, ()) + + def _on_get_dbus_names_error(self, e): + logging.basicConfig() + logger.error('GetDBusNames failed: %s', e) + + def _on_dbus_names_changed(self, tube_id, added, removed): + if tube_id == self.tube_id: + for handle, bus_name in added: + if handle == self.self_handle: + # I've just joined - set my unique name + self.set_unique_name(bus_name) + self.participants[handle] = bus_name + self.bus_name_to_handle[bus_name] = handle + + # call the callback while the removed people are still in + # participants, so their bus names are available + for callback in self._mapping_watches: + callback(added, removed) + + for handle in removed: + bus_name = self.participants.pop(handle, None) + self.bus_name_to_handle.pop(bus_name, None) + + def watch_participants(self, callback): + self._mapping_watches.append(callback) + if self.participants: + # GetDBusNames already returned: fake a participant add event + # immediately + added = [] + for k, v in self.participants.iteritems(): + added.append((k, v)) + callback(added, []) diff --git a/webactivity.py b/webactivity.py index 0b3cfa4..5db55f3 100755 --- a/webactivity.py +++ b/webactivity.py @@ -20,9 +20,15 @@ from gettext import gettext as _ import gtk import dbus +import sha from sugar.activity import activity from sugar import env +from sugar.graphics import style +import telepathy +import telepathy.client +from sugar import _sugarext +from sugar.presence import presenceservice import hulahop hulahop.startup(os.path.join(env.get_profile_path(), 'gecko')) @@ -33,26 +39,35 @@ import downloadmanager import promptservice import securitydialogs import filepicker -import sessionhistory +import sessionhistory import progresslistener _LIBRARY_PATH = '/home/olpc/Library/index.html' +from linktoolbar import LinkToolbar +from model import Model +from tubeconn import TubeConnection +from messenger import Messenger + +SERVICE = "org.laptop.WebActivity" +IFACE = SERVICE +PATH = "/org/laptop/WebActivity" + +_logger = logging.getLogger('web-activity') + + class WebActivity(activity.Activity): def __init__(self, handle, browser=None): - activity.Activity.__init__(self, handle) - - logging.debug('Starting the web activity') + activity.Activity.__init__(self, handle) + + _logger.debug('Starting the web activity') if browser: self._browser = browser else: self._browser = Browser() - - self.set_canvas(self._browser) - self._browser.show() - - temp_path = os.path.join(self.get_activity_root(), 'tmp') + + temp_path = os.path.join(self.get_activity_root(), 'tmp') downloadmanager.init(self._browser, temp_path) sessionhistory.init(self._browser) progresslistener.init(self._browser) @@ -60,13 +75,39 @@ class WebActivity(activity.Activity): toolbox = activity.ActivityToolbox(self) activity_toolbar = toolbox.get_activity_toolbar() - toolbar = WebToolbar(self._browser) - toolbox.add_toolbar(_('Browse'), toolbar) - toolbar.show() + self.toolbar = WebToolbar(self._browser) + toolbox.add_toolbar(_('Browse'), self.toolbar) + self.toolbar.show() self.set_toolbox(toolbox) toolbox.show() + self.linkbar = LinkToolbar() + self.linkbar.connect('link-selected', self._link_selected_cb) + self.linkbar.connect('link-rm', self._link_rm_cb) + self.session_history = sessionhistory.get_instance() + self.session_history.connect('session-link-changed', self._session_history_changed_cb) + self.toolbar._add_link.connect('clicked', self._add_link_button_cb) + + self._browser.connect("notify::title", self._title_changed_cb) + self.model = Model(os.path.dirname(__file__)) + + self._main_view = gtk.VBox() + self.set_canvas(self._main_view) + self._main_view.show() + + self._main_view.pack_start(self._browser) + self._browser.show() + + self._main_view.pack_start(self.linkbar, expand=False) + self.linkbar.show() + + self.current = _('blank') + self.webtitle = _('blank') + self.connect('key-press-event', self.key_press_cb) + self.sname = _sugarext.get_prgname() + _logger.debug('ProgName: %s' %self.sname) + if handle.uri: self._browser.load_uri(handle.uri) elif not self._jobject.file_path and not browser: @@ -74,27 +115,163 @@ class WebActivity(activity.Activity): # opening URIs and default docs. self._load_homepage() + _sugarext.set_prgname(self.sname) + + self.set_title('WebActivity') + self.messenger = None + self.connect('shared', self._shared_cb) + + # Get the Presence Service + self.pservice = presenceservice.get_instance() + name, path = self.pservice.get_preferred_connection() + self.tp_conn_name = name + self.tp_conn_path = path + self.conn = telepathy.client.Connection(name, path) + self.initiating = None + + if self._shared_activity is not None: + _logger.debug('shared: %s' %self._shared_activity.props.joined) + + self.owner = self.pservice.get_owner() + if self._shared_activity is not None: + # We are joining the activity + _logger.debug('Joined activity') + self.connect('joined', self._joined_cb) + if self.get_shared(): + # We've already joined + self._joined_cb() + else: + _logger.debug('Created activity') + + + def _shared_cb(self, activity): + _logger.debug('My activity was shared') + self.initiating = True + self._setup() + + _logger.debug('This is my activity: making a tube...') + id = self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferTube( + telepathy.TUBE_TYPE_DBUS, SERVICE, {}) + + + def _setup(self): + if self._shared_activity is None: + _logger.debug('Failed to share or join activity') + return + + bus_name, conn_path, channel_paths = self._shared_activity.get_channels() + + # Work out what our room is called and whether we have Tubes already + room = None + tubes_chan = None + text_chan = None + for channel_path in channel_paths: + channel = telepathy.client.Channel(bus_name, channel_path) + htype, handle = channel.GetHandle() + if htype == telepathy.HANDLE_TYPE_ROOM: + _logger.debug('Found our room: it has handle#%d "%s"' + %(handle, self.conn.InspectHandles(htype, [handle])[0])) + room = handle + ctype = channel.GetChannelType() + if ctype == telepathy.CHANNEL_TYPE_TUBES: + _logger.debug('Found our Tubes channel at %s'%channel_path) + tubes_chan = channel + elif ctype == telepathy.CHANNEL_TYPE_TEXT: + _logger.debug('Found our Text channel at %s'%channel_path) + text_chan = channel + + if room is None: + _logger.debug("Presence service didn't create a room") + return + if text_chan is None: + _logger.debug("Presence service didn't create a text channel") + return + + # Make sure we have a Tubes channel - PS doesn't yet provide one + if tubes_chan is None: + _logger.debug("Didn't find our Tubes channel, requesting one...") + tubes_chan = self.conn.request_channel(telepathy.CHANNEL_TYPE_TUBES, + telepathy.HANDLE_TYPE_ROOM, room, True) + + self.tubes_chan = tubes_chan + self.text_chan = text_chan + + tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube', + self._new_tube_cb) + + def _list_tubes_reply_cb(self, tubes): + for tube_info in tubes: + self._new_tube_cb(*tube_info) + + def _list_tubes_error_cb(self, e): + _logger.debug('ListTubes() failed: %s'%e) + + def _joined_cb(self, activity): + if not self._shared_activity: + return + + _logger.debug('Joined an existing shared activity') + + self.initiating = False + self._setup() + + _logger.debug('This is not my activity: waiting for a tube...') + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes( + reply_handler=self._list_tubes_reply_cb, + error_handler=self._list_tubes_error_cb) + + def _new_tube_cb(self, id, initiator, type, service, params, state): + _logger.debug('New tube: ID=%d initator=%d type=%d service=%s ' + 'params=%r state=%d' %(id, initiator, type, service, + params, state)) + + if (type == telepathy.TUBE_TYPE_DBUS and + service == SERVICE): + if state == telepathy.TUBE_STATE_LOCAL_PENDING: + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].AcceptTube(id) + + self.tube_conn = TubeConnection(self.conn, + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES], + id, group_iface=self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP]) + + _logger.debug('Tube created') + self.messenger = Messenger(self.tube_conn, self.initiating, self.model, self.linkbar, self.owner) + + def _load_homepage(self): if os.path.isfile(_LIBRARY_PATH): self._browser.load_uri('file://' + _LIBRARY_PATH) else: self._browser.load_uri('about:blank') + _sugarext.set_prgname(self.sname) + def _session_history_changed_cb(self, session_history, link): + _logger.debug('NewPage: %s.' %link) + self.current = link + def _title_changed_cb(self, embed, pspec): - self.set_title(embed.props.title) - + if embed.props.title is not '': + #self.set_title(embed.props.title) + _logger.debug('Title changed=%s' % embed.props.title) + self.webtitle = embed.props.title + _sugarext.set_prgname("org.laptop.WebActivity") + def read_file(self, file_path): if self.metadata['mime_type'] == 'text/plain': - f = open(file_path, 'r') - try: - session_data = f.read() - finally: - f.close() - logging.debug('Trying to set session: %s.' % session_data) - self._browser.set_session(session_data) + self.model.read(file_path) + i=0 + for link in self.model.links: + _logger.debug('read: url=%s title=%s d=%s' % (link['url'], link['title'], link['color'])) + if link['deleted'] == 0: + self.linkbar._add_link(link['url'], link['thumb'], link['color'], link['title'], link['owner'], i) + i+=1 + + if self.model.session_data is not '': + self._browser.set_session(self.model.session_data) else: self._browser.load_uri(file_path) - + _sugarext.set_prgname(self.sname) + def write_file(self, file_path): if not self.metadata['mime_type']: self.metadata['mime_type'] = 'text/plain' @@ -104,14 +281,13 @@ class WebActivity(activity.Activity): if self._browser.props.title: self.metadata['title'] = self._browser.props.title - session_data = self._browser.get_session() - if session_data: - f = open(file_path, 'w') - try: - f.write(session_data) - finally: - f.close() + for link in self.model.links: + _logger.debug('write: url=%s title=%s d=%s' % (link['url'], link['title'], link['color'])) + self.model.session_data = self._browser.get_session() + _logger.debug('Trying save session: %s.' % self.model.session_data) + self.model.write(file_path) + def destroy(self): if downloadmanager.can_quit(): activity.Activity.destroy(self) @@ -119,6 +295,67 @@ class WebActivity(activity.Activity): downloadmanager.set_quit_callback(self._quit_callback_cb) def _quit_callback_cb(self): - logging.debug('_quit_callback_cb') + _logger.debug('_quit_callback_cb') activity.Activity.destroy(self) + def _link_selected_cb(self, linkbar, link): + self._browser.load_uri(link) + + def _link_rm_cb(self, linkbar, index): + self.model.links[index]['deleted'] = 1 + self.model.links[index]['thumb'] = '' + + def _add_link_button_cb(self, button): + self._add_link() + + def key_press_cb(self, widget, event): + if event.state & gtk.gdk.CONTROL_MASK: + if gtk.gdk.keyval_name(event.keyval) == "l": + self._add_link() + return True + elif gtk.gdk.keyval_name(event.keyval) == "r": + _logger.debug('keyboard: Remove link: %s.' % self.current) + current = self.linkbar._rm_link() + self.model.links[current]['deleted'] = 1 + self.model.links[current]['thumb'] = '' + return True + return False + + def _add_link(self): + buffer = self._get_screenshot() + _logger.debug('keyboard: Add link: %s.' % self.current) + self.model.links.append( {'hash':sha.new(self.current).hexdigest(), 'url':self.current, 'title':self.webtitle, + 'thumb':buffer, 'owner':self.owner.props.nick, 'color':self.owner.props.color, 'deleted':0} ) + + self.linkbar._add_link(self.current, buffer, self.owner.props.color, self.webtitle, self.owner.props.nick, + len(self.model.links)-1) + if self.messenger is not None: + import base64 + self.messenger._add_link(self.current, self.webtitle, self.owner.props.color, + self.owner.props.nick, base64.b64encode(buffer)) + + def _pixbuf_save_cb(self, buf, data): + data[0] += buf + return True + + def get_buffer(self, pixbuf): + data = [""] + pixbuf.save_to_callback(self._pixbuf_save_cb, "png", {}, data) + return str(data[0]) + + + def _get_screenshot(self): + window = self._browser.window + width, height = window.get_size() + + screenshot = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, has_alpha=False, + bits_per_sample=8, width=width, height=height) + screenshot.get_from_drawable(window, window.get_colormap(), 0, 0, 0, 0, + width, height) + + screenshot = screenshot.scale_simple(style.zoom(160), + style.zoom(120), + gtk.gdk.INTERP_BILINEAR) + + buffer = self.get_buffer(screenshot) + return buffer diff --git a/webtoolbar.py b/webtoolbar.py index a330d70..6735a9c 100755 --- a/webtoolbar.py +++ b/webtoolbar.py @@ -67,6 +67,11 @@ class WebToolbar(gtk.Toolbar): self.insert(entry_item, -1) entry_item.show() + self._add_link = ToolButton('add-link') + self._add_link.set_tooltip(_('Add Link')) + self.insert(self._add_link, -1) + self._add_link.show() + progress_listener = progresslistener.get_instance() progress_listener.connect('location-changed', self._location_changed_cb) progress_listener.connect('loading-start', self._loading_start_cb) |