Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xactivity/activity.info2
-rw-r--r--browser.dtd21
-rw-r--r--icons/add-link.svg13
-rw-r--r--icons/buddy-link.svg23
-rw-r--r--linkbutton.py89
-rw-r--r--linktoolbar.py94
-rw-r--r--messenger.py126
-rw-r--r--model.py178
-rw-r--r--po/Web.pot30
-rw-r--r--sessionhistory.py9
-rw-r--r--tubeconn.py107
-rwxr-xr-xwebactivity.py297
-rwxr-xr-xwebtoolbar.py5
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
diff --git a/po/Web.pot b/po/Web.pot
index e80de79..38fae2e 100644
--- a/po/Web.pot
+++ b/po/Web.pot
@@ -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)