Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bookmarklets.py86
-rw-r--r--bookmarklettoolbar.py95
-rw-r--r--browser.py32
-rw-r--r--edittoolbar.py50
-rw-r--r--icons/activity-ssb.svg13
-rw-r--r--icons/bookmarklet-inverted.svg6
-rw-r--r--icons/bookmarklet-thick.svg6
-rw-r--r--icons/bookmarklet.svg6
-rw-r--r--palettes.py25
-rw-r--r--ssb.py169
-rw-r--r--usercode.py250
-rw-r--r--webactivity.py119
-rw-r--r--webtoolbar.py78
13 files changed, 902 insertions, 33 deletions
diff --git a/bookmarklets.py b/bookmarklets.py
new file mode 100644
index 0000000..c6d007a
--- /dev/null
+++ b/bookmarklets.py
@@ -0,0 +1,86 @@
+# Copyright (C) 2009, Lucian Branescu Mihaila
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import ConfigParser
+import os
+import gobject
+import logging
+
+from sugar.activity import activity
+
+_store = None
+
+def get_store():
+ global _store
+ if _store is None:
+ _store = BookmarkletStore()
+ return _store
+
+class BookmarkletStore(gobject.GObject):
+ __gsignals__ = {
+ 'add_bookmarklet': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([str])),
+ 'overwrite_bookmarklet': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([str, str])),
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._config = ConfigParser.RawConfigParser()
+ self.config_path = activity.get_activity_root()
+ self.config_path = os.path.join(self.config_path,
+ 'data/bookmarklets.ini')
+ self._config.read(self.config_path)
+
+ def __del__(self):
+ '''Save bookmarklets (usually when the activity is closed)'''
+ self.write()
+
+ def write(self):
+ # create data/ssb dir if it doesn't exist
+ dir_path = os.path.dirname(self.config_path)
+ if not os.path.isdir(dir_path):
+ os.mkdir(dir_path)
+
+ # write config
+ f = open(self.config_path, 'w')
+ self._config.write(f)
+ f.close()
+
+ def list(self):
+ return self._config.sections()
+
+ def remove(self, name):
+ self._config.remove_section(name)
+ self.write()
+
+ def get(self, name):
+ return self._config.get(name, 'url')
+
+ def add(self, name, url):
+ if not self._config.has_section(name):
+ self._config.add_section(name)
+ self._config.set(name, 'url', url)
+ self.write()
+ elif self.get(name) != url:
+ self.emit('overwrite_bookmarklet', name, url)
+
+ # we don't care if the bookmarklet was added just now
+ if self._config.has_section(name) and self.get(name) == url:
+ self.emit('add_bookmarklet', name)
+
+
diff --git a/bookmarklettoolbar.py b/bookmarklettoolbar.py
new file mode 100644
index 0000000..18d93e9
--- /dev/null
+++ b/bookmarklettoolbar.py
@@ -0,0 +1,95 @@
+# Copyright (C) 2009, Lucian Branescu Mihaila
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+from gettext import gettext as _
+import os
+import gtk
+import logging
+import gobject
+
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics.palette import Palette
+
+import bookmarklets
+
+# HACK until we have toolbox.get_toolbars()
+_TOOLBAR_BROWSE = 2
+_TOOLBAR_BOOKMARKLETS = 4
+
+class BookmarkletButton(ToolButton):
+ def __init__(self, toolbar, name, uri):
+ self._name = name
+ self._uri = uri
+ self._toolbar = toolbar
+ self._browser = toolbar._activity._browser
+
+ # set up the button
+ ToolButton.__init__(self, 'bookmarklet')
+ self.connect('clicked', self._clicked_cb)
+ toolbar.insert(self, -1)
+
+ # and its palette
+ palette = Palette(name, text_maxlen=50)
+ self.set_palette(palette)
+
+ menu_item = gtk.MenuItem(_('Remove'))
+ menu_item.connect('activate', self._remove_cb)
+ palette.menu.append(menu_item)
+ menu_item.show()
+
+ self.show()
+
+ def animate(self):
+ gobject.timeout_add(500, self.set_icon, 'bookmarklet-thick')
+ gobject.timeout_add(800, self.set_icon, 'bookmarklet')
+
+ def flash(self):
+ gobject.timeout_add(500, self.set_icon, 'bookmarklet-inverted')
+ gobject.timeout_add(800, self.set_icon, 'bookmarklet')
+
+ def _clicked_cb(self, button):
+ self._browser.load_uri(self._uri)
+
+ def _remove_cb(self, widget):
+ bookmarklets.get_store().remove(self._name)
+ self.destroy()
+
+ def destroy(self):
+ del self._toolbar.bookmarklets[self._name]
+
+ if len(self._toolbar.bookmarklets) == 0:
+ self._toolbar.destroy()
+
+ ToolButton.destroy(self)
+
+class BookmarkletToolbar(gtk.Toolbar):
+ def __init__(self, activity):
+ gtk.Toolbar.__init__(self)
+
+ self._activity = activity
+ self._browser = self._activity._browser
+
+ self.bookmarklets = {}
+
+ def add_bookmarklet(self, name):
+ url = bookmarklets.get_store().get(name)
+ self.bookmarklets[name] = BookmarkletButton(self, name, url)
+
+ def destroy(self):
+ self._activity.toolbox.remove_toolbar(_TOOLBAR_BOOKMARKLETS)
+ self._activity.toolbox.set_current_toolbar(_TOOLBAR_BROWSE)
+
+ gtk.Toolbar.destroy(self) \ No newline at end of file
diff --git a/browser.py b/browser.py
index b0a7ae7..a03740e 100644
--- a/browser.py
+++ b/browser.py
@@ -39,6 +39,7 @@ import sessionstore
from palettes import ContentInvoker
from sessionhistory import HistoryListener
from progresslistener import ProgressListener
+from usercode import ScriptListener
_ZOOM_AMOUNT = 0.1
@@ -89,14 +90,15 @@ class Browser(WebView):
AGENT_SHEET = os.path.join(activity.get_bundle_path(),
'agent-stylesheet.css')
- USER_SHEET = os.path.join(env.get_profile_path(), 'gecko',
- 'user-stylesheet.css')
+ USER_SHEET = os.path.join(activity.get_activity_root(),
+ 'data/style.user.css')
def __init__(self):
WebView.__init__(self)
-
+
self.history = HistoryListener()
self.progress = ProgressListener()
+ self.userscript = ScriptListener()
cls = components.classes["@mozilla.org/typeaheadfind;1"]
self.typeahead = cls.createInstance(interfaces.nsITypeAheadFind)
@@ -113,21 +115,23 @@ class Browser(WebView):
io_service2.manageOfflineStatus = False
cls = components.classes['@mozilla.org/content/style-sheet-service;1']
- style_sheet_service = cls.getService(interfaces.nsIStyleSheetService)
+ self.style_sheet_service = cls.getService(
+ interfaces.nsIStyleSheetService)
if os.path.exists(Browser.AGENT_SHEET):
agent_sheet_uri = io_service.newURI('file:///' +
Browser.AGENT_SHEET,
None, None)
- style_sheet_service.loadAndRegisterSheet(agent_sheet_uri,
+ self.style_sheet_service.loadAndRegisterSheet(agent_sheet_uri,
interfaces.nsIStyleSheetService.AGENT_SHEET)
if os.path.exists(Browser.USER_SHEET):
- user_sheet_uri = io_service.newURI('file:///' + Browser.USER_SHEET,
+ self.user_sheet_uri = io_service.newURI('file:///' +
+ Browser.USER_SHEET,
None, None)
- style_sheet_service.loadAndRegisterSheet(user_sheet_uri,
+ self.style_sheet_service.loadAndRegisterSheet(self.user_sheet_uri,
interfaces.nsIStyleSheetService.USER_SHEET)
-
+
def do_setup(self):
WebView.do_setup(self)
@@ -142,14 +146,24 @@ class Browser(WebView):
self.progress.setup(self)
self.history.setup(self.web_navigation)
+
+ self.userscript.setup(self)
self.typeahead.init(self.doc_shell)
-
+
def get_session(self):
return sessionstore.get_session(self)
def set_session(self, data):
return sessionstore.set_session(self, data)
+
+ def update_userstyle(self):
+ if self.style_sheet_service.sheetRegistered(self.user_sheet_uri,
+ interfaces.nsIStyleSheetService.USER_SHEET):
+ self.style_sheet_service.unregisterSheet(self.user_sheet_uri,
+ interfaces.nsIStyleSheetService.USER_SHEET)
+ self.style_sheet_service.loadAndRegisterSheet(self.user_sheet_uri,
+ interfaces.nsIStyleSheetService.USER_SHEET)
def get_source(self, async_cb, async_err_cb):
cls = components.classes[ \
diff --git a/edittoolbar.py b/edittoolbar.py
index 08ebd76..f180911 100644
--- a/edittoolbar.py
+++ b/edittoolbar.py
@@ -25,6 +25,8 @@ from sugar.graphics import iconentry
from sugar.graphics.toolbutton import ToolButton
from sugar.graphics import style
+import usercode
+
class EditToolbar(activity.EditToolbar):
_com_interfaces_ = interfaces.nsIObserver
@@ -99,6 +101,54 @@ class EditToolbar(activity.EditToolbar):
self._next.connect('clicked', self.__find_next_cb)
self.insert(self._next, -1)
self._next.show()
+
+ separator = gtk.SeparatorToolItem()
+ separator.set_draw(False)
+ separator.set_expand(True)
+ self.insert(separator, -1)
+ separator.show()
+
+ self.edit_userstyle = ToolButton('edit-userstyle')
+ self.edit_userstyle.set_tooltip('Edit user CSS')
+ self.edit_userstyle.connect('clicked', self.__edit_userstyle_cb)
+ self.insert(self.edit_userstyle, -1)
+ self.edit_userstyle.show()
+
+ self.edit_userscripts = ToolButton('edit-userscripts')
+ self.edit_userscripts.set_tooltip('Edit user scripts')
+ self.edit_userscripts.connect('clicked', self.__edit_userscripts_cb)
+ self.insert(self.edit_userscripts, -1)
+ self.edit_userscripts.show()
+
+ def __edit_userstyle_cb(self, button):
+ #editor = usercode.StyleEditor()
+ #editor.connect('userstyle-changed', self.__update_userstyle_cb)
+ #editor.show()
+
+ editor = usercode.SourceEditor(mime_type='text/css')
+ w = gtk.Window()
+ w.add(editor)
+ w.show_all()
+ w.show()
+
+ def __update_userstyle_cb(self, editor):
+ self._browser.update_userstyle()
+
+ def __edit_userscripts_cb(self, button):
+ editor = usercode.ScriptEditor()
+ editor.connect('inject-script', self.__inject_script_cb)
+ editor.show()
+
+ def __inject_script_cb(self, editor, text):
+ doc = self._browser.dom_window.document
+
+ head = doc.getElementsByTagName('head').item(0)
+
+ script = doc.createElement('script')
+ script.type = 'text/javascript'
+ script.appendChild(doc.createTextNode(text))
+
+ head.appendChild(script)
def __undo_cb(self, button):
command_manager = self._get_command_manager()
diff --git a/icons/activity-ssb.svg b/icons/activity-ssb.svg
new file mode 100644
index 0000000..3f40927
--- /dev/null
+++ b/icons/activity-ssb.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#010101">
+ <!ENTITY fill_color "#FFFFFF">
+]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="activity-ssb">
+ <circle cx="27.375" cy="27.5" display="inline" fill="&fill_color;" r="19.903" stroke="&stroke_color;" stroke-width="3.5"/>
+ <g display="inline">
+ <path d="M27.376,7.598c0,0-11.205,8.394-11.205,19.976 c0,11.583,11.205,19.829,11.205,19.829" fill="&fill_color;" stroke="&stroke_color;" stroke-width="3.5"/>
+ <path d="M27.376,7.598c0,0,11.066,9.141,11.066,19.976 c0,10.839-11.066,19.829-11.066,19.829" fill="&fill_color;" stroke="&stroke_color;" stroke-width="3.5"/>
+ <line fill="&fill_color;" stroke="&stroke_color;" stroke-width="3.5" x1="27.376" x2="27.376" y1="7.598" y2="47.402"/>
+ <line fill="&fill_color;" stroke="&stroke_color;" stroke-width="3.5" x1="27.376" x2="27.376" y1="7.598" y2="47.402"/>
+ </g>
+</g></svg>
+
diff --git a/icons/bookmarklet-inverted.svg b/icons/bookmarklet-inverted.svg
new file mode 100644
index 0000000..fa3c1fc
--- /dev/null
+++ b/icons/bookmarklet-inverted.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#666666">
+ <!ENTITY fill_color "#ffffff">
+]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="bookmarklet">
+ <polygon fill="&stroke_color;" points="27.5,5.149 34.76,19.865 51,22.224 39.251,33.68 42.025,49.852 27.5,42.215 12.976,49.852 15.75,33.68 4,22.224 20.237,19.865 " stroke="&fill_color;" stroke-linecap="round" stroke-width="3.5"/>
+</g></svg> \ No newline at end of file
diff --git a/icons/bookmarklet-thick.svg b/icons/bookmarklet-thick.svg
new file mode 100644
index 0000000..ad0371a
--- /dev/null
+++ b/icons/bookmarklet-thick.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#666666">
+ <!ENTITY fill_color "#ffffff">
+]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="bookmarklet">
+ <polygon fill="&fill_color;" points="27.5,5.149 34.76,19.865 51,22.224 39.251,33.68 42.025,49.852 27.5,42.215 12.976,49.852 15.75,33.68 4,22.224 20.237,19.865 " stroke="&stroke_color;" stroke-linecap="round" stroke-width="7"/>
+</g></svg> \ No newline at end of file
diff --git a/icons/bookmarklet.svg b/icons/bookmarklet.svg
new file mode 100644
index 0000000..9d106bb
--- /dev/null
+++ b/icons/bookmarklet.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#666666">
+ <!ENTITY fill_color "#ffffff">
+]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="bookmarklet">
+ <polygon fill="&fill_color;" points="27.5,5.149 34.76,19.865 51,22.224 39.251,33.68 42.025,49.852 27.5,42.215 12.976,49.852 15.75,33.68 4,22.224 20.237,19.865 " stroke="&stroke_color;" stroke-linecap="round" stroke-width="3.5"/>
+</g></svg> \ No newline at end of file
diff --git a/palettes.py b/palettes.py
index 1f3bfc2..c86d8dd 100644
--- a/palettes.py
+++ b/palettes.py
@@ -31,6 +31,7 @@ from sugar import profile
from sugar.activity import activity
import downloadmanager
+import bookmarklets
class ContentInvoker(Invoker):
_com_interfaces_ = interfaces.nsIDOMEventListener
@@ -54,6 +55,7 @@ class ContentInvoker(Invoker):
return
target = event.target
+
if target.tagName.lower() == 'a':
if target.firstChild:
@@ -85,7 +87,7 @@ class LinkPalette(Palette):
self._title = title
self._url = url
self._owner_document = owner_document
-
+
if title is not None:
self.props.primary_text = title
self.props.secondary_text = url
@@ -104,11 +106,19 @@ class LinkPalette(Palette):
menu_item.connect('activate', self.__copy_activate_cb)
self.menu.append(menu_item)
menu_item.show()
-
- menu_item = MenuItem(_('Download link'))
- menu_item.connect('activate', self.__download_activate_cb)
- self.menu.append(menu_item)
- menu_item.show()
+
+ if url.startswith('javascript:'):
+ # only show in an ssb, if the link is a bookmarklet
+ menu_item = MenuItem(_('Save bookmarklet'))
+ menu_item.connect('activate', self.__bookmarklet_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
+ else:
+ # for all other links
+ menu_item = MenuItem(_('Download link'))
+ menu_item.connect('activate', self.__download_activate_cb)
+ self.menu.append(menu_item)
+ menu_item.show()
def __follow_activate_cb(self, menu_item):
self._browser.load_uri(self._url)
@@ -142,6 +152,9 @@ class LinkPalette(Palette):
def __download_activate_cb(self, menu_item):
downloadmanager.save_link(self._url, self._title, self._owner_document)
+
+ def __bookmarklet_activate_cb(self, menu_item):
+ bookmarklets.get_store().add(self._title, self._url)
class ImagePalette(Palette):
def __init__(self, title, url, owner_document):
diff --git a/ssb.py b/ssb.py
new file mode 100644
index 0000000..d75a89d
--- /dev/null
+++ b/ssb.py
@@ -0,0 +1,169 @@
+# Copyright (C) 2009, Lucian Branescu Mihaila
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import shutil
+import os
+import tempfile
+import zipfile
+import ConfigParser
+import logging
+import functools
+
+from sugar.activity import activity
+from sugar.activity import bundlebuilder
+from sugar.bundle.activitybundle import ActivityBundle
+from sugar.datastore import datastore
+from sugar import profile
+
+DOMAIN_PREFIX = 'org.sugarlabs.ssb'
+
+def get_is_ssb(activity):
+ '''determine if the activity is an SSB'''
+ return activity.get_bundle_id().startswith(DOMAIN_PREFIX)
+
+# freeze some arguments, equivalent to def list_files(path): ...
+list_files = functools.partial(bundlebuilder.list_files,
+ ignore_dirs=bundlebuilder.IGNORE_DIRS,
+ ignore_files=bundlebuilder.IGNORE_FILES.append('.DS_STORE'))
+
+def remove_paths(paths, root=None):
+ '''remove all paths in the list, fail silently'''
+ if root is not None:
+ paths = [os.path.join(root, i) for i in paths]
+
+ for path in paths:
+ try:
+ if os.path.isdir(path):
+ shutil.rmtree(path)
+ else:
+ os.remove(path)
+ except OSError:
+ logging.warning('failed to remove: ' + path)
+
+def copy_profile():
+ '''get the data from the bundle and into the profile'''
+ ssb_data_path = os.path.join(activity.get_bundle_path(), 'data/ssb_data')
+ data_path = os.path.join(activity.get_activity_root(), 'data')
+
+ if os.path.isdir(ssb_data_path):
+ # we can't use shutil.copytree for the entire dir
+ for i in os.listdir(ssb_data_path):
+ src = os.path.join(ssb_data_path, i)
+ dst = os.path.join(data_path, i)
+ if not os.path.exists(dst):
+ if os.path.isdir(src):
+ shutil.copytree(src, dst)
+ else: # is there a better way?
+ shutil.copy(src, dst)
+
+class SSBCreator(object):
+ def __init__(self, title, uri):
+ self.title = title
+ self.name = title.replace(' ', '')
+ self.uri = uri
+ self.bundle_id = '%s.%sActivity' % (DOMAIN_PREFIX, self.name)
+
+ self.bundle_path = activity.get_bundle_path()
+ self.data_path = os.path.join(activity.get_activity_root(), 'data')
+ self.temp_path = tempfile.mkdtemp() # make sure there's no collisions
+ self.ssb_path = os.path.join(self.temp_path, self.name + '.activity')
+
+ def __del__(self):
+ '''clean up after ourselves, fail silently'''
+ shutil.rmtree(self.temp_path, ignore_errors=True)
+
+ def change_info(self):
+ '''change the .info file accordingly'''
+ path = os.path.join(self.ssb_path, 'activity/activity.info')
+
+ config = ConfigParser.RawConfigParser()
+ config.read(path)
+
+ if config.get('Activity', 'name') == 'Browse':
+ version = 1
+ else:
+ version = int(config.get('Activity', 'activity_version')) + 1
+
+ config.set('Activity', 'activity_version', version)
+ config.set('Activity', 'name', self.title)
+ config.set('Activity', 'bundle_id', self.bundle_id)
+ config.set('Activity', 'icon', 'activity-ssb')
+
+ # write the changes
+ f = open(path, 'w')
+ config.write(f)
+ f.close()
+
+ def create(self):
+ '''actual creation'''
+ # copy the bundle
+ shutil.copytree(self.bundle_path, self.ssb_path)
+
+ self.change_info()
+
+ # add the ssb icon
+ shutil.copy(os.path.join(self.ssb_path, 'icons/activity-ssb.svg'),
+ os.path.join(self.ssb_path, 'activity'))
+
+ # set homepage
+ f = open(os.path.join(self.ssb_path, 'data/homepage'), 'w')
+ f.write(self.uri)
+ f.close()
+
+ # copy profile
+ ssb_data_path = os.path.join(self.ssb_path, 'data/ssb_data')
+ shutil.copytree(self.data_path, ssb_data_path)
+
+ # delete undesirable things from the profile
+ remove_paths(['Cache', 'cookies.sqlite'],
+ root=os.path.join(ssb_data_path, 'gecko'))
+
+ # create MANIFEST
+ files = list_files(self.ssb_path)
+ f = open(os.path.join(self.ssb_path, 'MANIFEST'), 'w')
+ for i in files:
+ f.write(i+'\n')
+ f.close()
+
+ # create .xo bundle
+ # include the manifest
+ files.append('MANIFEST')
+
+ self.xo_path = os.path.join(self.temp_path, self.name.lower() + '.xo')
+
+ # zip everything
+ xo = zipfile.ZipFile(self.xo_path, 'w', zipfile.ZIP_DEFLATED)
+ for i in files:
+ xo.write(os.path.join(self.ssb_path, i),
+ os.path.join(self.name + '.activity', i))
+ xo.close()
+
+ def install(self):
+ '''install the generated .xo bundle'''
+ bundle = ActivityBundle(self.xo_path)
+ bundle.install()
+
+ def show_in_journal(self):
+ '''send the generated .xo bundle to the journal'''
+ jobject = datastore.create()
+ jobject.metadata['title'] = self.title
+ jobject.metadata['mime_type'] = 'application/vnd.olpc-sugar'
+ jobject.metadata['icon-color'] = profile.get_color().to_string()
+ jobject.file_path = self.xo_path
+
+ datastore.write(jobject)
+
+ activity.show_object_in_journal(jobject.object_id) \ No newline at end of file
diff --git a/usercode.py b/usercode.py
new file mode 100644
index 0000000..e6f6c02
--- /dev/null
+++ b/usercode.py
@@ -0,0 +1,250 @@
+# Copyright (C) 2009, Lucian Branescu Mihaila
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import logging
+from gettext import gettext as _
+
+import gobject
+import gtk
+import pango
+import gtksourceview2
+
+import xpcom
+from xpcom.components import interfaces
+
+from sugar.activity import activity
+from sugar.graphics import style
+from sugar.graphics.icon import Icon
+
+
+class SourceEditor(gtk.ScrolledWindow):
+ '''TextView-like widget with syntax coloring and scroll bars
+
+ Much of the initialisation code is from Pippy'''
+
+ __gtype_name__ = 'SugarSourceEditor'
+
+ def __init__(self, mime_type='text/plain', width=None, height=None):
+ gtk.ScrolledWindow.__init__(self)
+ self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+
+ self.mime_type = mime_type
+ self.width = width or int(gtk.gdk.screen_width()/2)
+ self.height = height or int(gtk.gdk.screen_height()/1.5)
+
+ self._buffer = gtksourceview2.Buffer()
+ lang_manager = gtksourceview2.language_manager_get_default()
+ if hasattr(lang_manager, 'list_languages'):
+ langs = lang_manager.list_languages()
+ else:
+ lang_ids = lang_manager.get_language_ids()
+ langs = [lang_manager.get_language(lang_id)
+ for lang_id in lang_ids]
+ for lang in langs:
+ for m in lang.get_mime_types():
+ if m == self.mime_type:
+ self._buffer.set_language(lang)
+
+ if hasattr(self._buffer, 'set_highlight'):
+ self._buffer.set_highlight(True)
+ else:
+ self._buffer.set_highlight_syntax(True)
+
+ # editor view
+ self._view = gtksourceview2.View(self._buffer)
+ self._view.set_size_request(self.width, self.height)
+ self._view.set_editable(True)
+ self._view.set_cursor_visible(True)
+ self._view.set_show_line_numbers(True)
+ self._view.set_wrap_mode(gtk.WRAP_CHAR)
+ self._view.set_auto_indent(True)
+ self._view.modify_font(pango.FontDescription("Monospace " +
+ str(style.FONT_SIZE)))
+
+ self.add(self._view)
+ self.show_all()
+
+ def get_text(self):
+ end = self._buffer.get_end_iter()
+ start = self._buffer.get_start_iter()
+ return self._buffer.get_text(start, end)
+
+ def set_text(self, text):
+ self._buffer.set_text(text)
+
+ text = property(get_text, set_text)
+
+class TextEditor(gtk.Window):
+ def __init__(self, mime_type='text/html', width=None, height=None):
+ gtk.Window.__init__(self)
+ self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+
+ self.mime_type = mime_type
+ self.width = width or int(gtk.gdk.screen_width()/2)
+ self.height = height or int(gtk.gdk.screen_height()/1.5)
+
+ # layout
+ vbox = gtk.VBox()
+ editorbox = gtk.HBox()
+ buttonbox = gtk.HBox()
+
+ # editor buffer
+ self.buffer = gtksourceview2.Buffer()
+ lang_manager = gtksourceview2.language_manager_get_default()
+ if hasattr(lang_manager, 'list_languages'):
+ langs = lang_manager.list_languages()
+ else:
+ lang_ids = lang_manager.get_language_ids()
+ langs = [lang_manager.get_language(lang_id)
+ for lang_id in lang_ids]
+ for lang in langs:
+ for m in lang.get_mime_types():
+ if m == self.mime_type:
+ self.buffer.set_language(lang)
+
+ if hasattr(self.buffer, 'set_highlight'):
+ self.buffer.set_highlight(True)
+ else:
+ self.buffer.set_highlight_syntax(True)
+
+ # editor view
+ view = gtksourceview2.View(self.buffer)
+ view.set_size_request(self.width, self.height)
+ view.set_editable(True)
+ view.set_cursor_visible(True)
+ view.set_show_line_numbers(True)
+ view.set_wrap_mode(gtk.WRAP_CHAR)
+ view.set_auto_indent(True)
+ view.modify_font(pango.FontDescription("Monospace " +
+ str(style.FONT_SIZE)))
+
+ codesw = gtk.ScrolledWindow()
+ codesw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ codesw.add(view)
+ #editorbox.pack_start(codesw)
+
+ #vbox.pack_start(editorbox)
+ vbox.pack_start(codesw)
+
+ # buttons
+ self._cancel_button = gtk.Button(label=_('Cancel'))
+ self._cancel_button.set_image(Icon(icon_name='dialog-cancel'))
+ self._cancel_button.connect('clicked', self._cancel_button_cb)
+ buttonbox.pack_start(self._cancel_button)
+
+ self._save_button = gtk.Button(label=_('Save'))
+ self._save_button.set_image(Icon(icon_name='dialog-ok'))
+ buttonbox.pack_start(self._save_button)
+
+ self._apply_button = gtk.Button(label=_('Apply'))
+ self._apply_button.set_image(Icon(icon_name='dialog-ok'))
+ buttonbox.pack_start(self._apply_button)
+
+ vbox.pack_start(buttonbox)
+ self.add(vbox)
+
+ def _cancel_button_cb(self, button):
+ self.destroy()
+
+ def show(self):
+ self.show_all()
+ gtk.Window.show(self)
+
+ def get_text(self):
+ end = self.buffer.get_end_iter()
+ start = self.buffer.get_start_iter()
+ return self.buffer.get_text(start, end)
+
+ def set_text(self, text):
+ self.buffer.set_text(text)
+
+ text = property(get_text, set_text)
+
+
+class StyleEditor(TextEditor):
+ __gsignals__ = {
+ 'userstyle-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([])),
+ }
+
+ def __init__(self):
+ TextEditor.__init__(self, mime_type='text/css')
+
+ self.css_path = os.path.join(activity.get_activity_root(),
+ 'data/style.user.css')
+
+ self._save_button.connect('clicked', self._save_button_cb)
+ self._apply_button.connect('clicked', self._apply_button_cb)
+
+ if os.path.isfile(self.css_path):
+ f = open(self.css_path, 'r')
+ self.text = f.read()
+ f.close()
+
+ def _apply_button_cb(self, button):
+ f = open(self.css_path, 'w')
+ f.write(self.text)
+ f.close()
+
+ self.emit('userstyle-changed')
+
+ def _save_button_cb(self, button):
+ self._apply_button_cb(button)
+
+ self.destroy()
+
+# TODO support multiple userscripts
+class ScriptEditor(TextEditor):
+ __gsignals__ = {
+ 'inject-script': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([str])),
+ }
+
+ def __init__(self):
+ TextEditor.__init__(self, mime_type='text/javascript')
+
+ self.script_path = os.path.join(activity.get_activity_root(),
+ 'data/script.user.js')
+
+ self._save_button.connect('clicked', self._save_button_cb)
+
+ def _save_button_cb(self, button):
+ self.emit('inject-script', self.text)
+
+ self.destroy()
+
+class ScriptListener(gobject.GObject):
+ _com_interfaces_ = interfaces.nsIWebProgressListener
+
+ __gsignals__ = {
+ 'userscript-found': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([])),
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+
+ self._wrapped_self = xpcom.server.WrapObject( \
+ self, interfaces.nsIWebProgressListener)
+
+ def onLocationChange(self, webProgress, request, location):
+ if location.spec.endswith('.user.js'):
+ self.emit('userscript-found')
+
+ def setup(self, browser):
+ browser.web_progress.addProgressListener(self._wrapped_self,
+ interfaces.nsIWebProgress.NOTIFY_LOCATION)
diff --git a/webactivity.py b/webactivity.py
index a7c55bb..0772706 100644
--- a/webactivity.py
+++ b/webactivity.py
@@ -30,6 +30,7 @@ import shutil
import sqlite3
import cjson
import gconf
+import shutil
# HACK: Needed by http://dev.sugarlabs.org/ticket/456
import gnome
@@ -42,10 +43,14 @@ import telepathy.client
from sugar.presence import presenceservice
from sugar.graphics.tray import HTray
from sugar import profile
-from sugar.graphics.alert import Alert
+from sugar.graphics.alert import Alert, ConfirmationAlert
from sugar.graphics.icon import Icon
from sugar import mime
+import ssb
+# get the profile saved in the ssb bundle, if needed
+ssb.copy_profile()
+
PROFILE_VERSION = 1
_profile_version = 0
@@ -124,7 +129,6 @@ def _seed_xs_cookie():
else:
_logger.debug('seed_xs_cookie: Updated cookie successfully')
-
import hulahop
hulahop.set_app_version(os.environ['SUGAR_BUNDLE_VERSION'])
hulahop.startup(_profile_path)
@@ -156,23 +160,29 @@ from browser import Browser
from edittoolbar import EditToolbar
from webtoolbar import WebToolbar
from viewtoolbar import ViewToolbar
+from bookmarklettoolbar import BookmarkletToolbar
import downloadmanager
import globalhistory
import filepicker
+import bookmarklets
_LIBRARY_PATH = '/usr/share/library-common/index.html'
+def _set_dbus_globals(bundle_id):
+ '''Set up the dbus strings, based on the bundle_id'''
+ global SERVICE, IFACE, PATH
+ SERVICE = bundle_id
+ IFACE = bundle_id
+ PATH = '/' + bundle_id.replace('.', '/')
+
from model import Model
from sugar.presence.tubeconn import TubeConnection
from messenger import Messenger
from linkbutton import LinkButton
-SERVICE = "org.laptop.WebActivity"
-IFACE = SERVICE
-PATH = "/org/laptop/WebActivity"
-
_TOOLBAR_EDIT = 1
_TOOLBAR_BROWSE = 2
+_TOOLBAR_BOOKMARKLETS = 4
_logger = logging.getLogger('web-activity')
@@ -181,12 +191,16 @@ class WebActivity(activity.Activity):
activity.Activity.__init__(self, handle)
_logger.debug('Starting the web activity')
+
+ # figure out if we're an SSB
+ self.is_ssb = ssb.get_is_ssb(self)
self._browser = Browser()
-
+
_set_accept_languages()
_seed_xs_cookie()
-
+ _set_dbus_globals(self.get_bundle_id())
+
# don't pick up the sugar theme - use the native mozilla one instead
cls = components.classes['@mozilla.org/preferences-service;1']
pref_service = cls.getService(components.interfaces.nsIPrefService)
@@ -199,7 +213,7 @@ class WebActivity(activity.Activity):
toolbox.add_toolbar(_('Edit'), self._edit_toolbar)
self._edit_toolbar.show()
- self._web_toolbar = WebToolbar(self._browser)
+ self._web_toolbar = WebToolbar(self)
toolbox.add_toolbar(_('Browse'), self._web_toolbar)
self._web_toolbar.show()
@@ -210,18 +224,28 @@ class WebActivity(activity.Activity):
self._view_toolbar = ViewToolbar(self)
toolbox.add_toolbar(_('View'), self._view_toolbar)
self._view_toolbar.show()
-
+
+ # the bookmarklet bar doesn't show up if empty
+ self._bm_toolbar = None
+
self.set_toolbox(toolbox)
- toolbox.show()
-
+ toolbox.show()
+
self.set_canvas(self._browser)
self._browser.show()
-
+
self._browser.history.connect('session-link-changed',
self._session_history_changed_cb)
self._web_toolbar.connect('add-link', self._link_add_button_cb)
self._browser.connect("notify::title", self._title_changed_cb)
+
+ self._bm_store = bookmarklets.get_store()
+ self._bm_store.connect('add_bookmarklet', self._add_bookmarklet_cb)
+ self._bm_store.connect('overwrite_bookmarklet',
+ self._overwrite_bookmarklet_cb)
+ for name in self._bm_store.list():
+ self._add_bookmarklet(name)
self.model = Model()
self.model.connect('add_link', self._add_link_model_cb)
@@ -231,7 +255,18 @@ class WebActivity(activity.Activity):
self.connect('key-press-event', self._key_press_cb)
self.toolbox.set_current_toolbar(_TOOLBAR_BROWSE)
-
+
+ if self.is_ssb:
+ # set permanent homepage for SSBs
+ f = open(os.path.join(activity.get_bundle_path(),
+ 'data/homepage'))
+ self.homepage = f.read()
+ f.close()
+
+ # enable userscript saving
+ self._browser.userscript.connect('userscript-found',
+ self._userscript_found_cb)
+
if handle.uri:
self._browser.load_uri(handle.uri)
elif not self._jobject.file_path:
@@ -364,7 +399,9 @@ class WebActivity(activity.Activity):
def _load_homepage(self):
- if os.path.isfile(_LIBRARY_PATH):
+ if self.is_ssb:
+ self._browser.load_uri(self.homepage)
+ elif os.path.isfile(_LIBRARY_PATH):
self._browser.load_uri('file://' + _LIBRARY_PATH)
else:
default_page = os.path.join(activity.get_bundle_path(),
@@ -460,6 +497,58 @@ class WebActivity(activity.Activity):
self._browser.zoom_in()
return True
return False
+
+ def _add_bookmarklet(self, name):
+ '''add bookmarklet button and, if needed, the toolbar'''
+ if self._bm_toolbar is None:
+ self._bm_toolbar = BookmarkletToolbar(self)
+ self.toolbox.add_toolbar(_('Bookmarklets'), self._bm_toolbar)
+ self._bm_toolbar.show()
+
+ if name not in self._bm_toolbar.bookmarklets:
+ self._bm_toolbar.add_bookmarklet(name)
+
+ return self._bm_toolbar.bookmarklets[name]
+
+ def _add_bookmarklet_cb(self, store, name):
+ '''receive name of new bookmarklet from the store'''
+ bm = self._add_bookmarklet(name)
+ bm.flash()
+
+ self.toolbox.set_current_toolbar(_TOOLBAR_BOOKMARKLETS)
+
+ def _overwrite_bookmarklet_cb(self, store, name, url):
+ '''Ask for confirmation'''
+ alert = ConfirmationAlert()
+ alert.props.title = _('Add bookmarklet')
+ alert.props.msg = _('"%s" already exists. Overwrite?') % name
+ alert.connect('response', self._overwrite_bookmarklet_response_cb)
+
+ # send the arguments through the alert
+ alert._bm = (name, url)
+
+ self.add_alert(alert)
+
+ def _overwrite_bookmarklet_response_cb(self, alert, response_id):
+ self.remove_alert(alert)
+
+ name, url = alert._bm
+ if response_id is gtk.RESPONSE_OK:
+ self._bm_store.remove(name)
+ self._bm_store.add(name, url)
+
+ def _userscript_found_cb(self, listener):
+ alert = ConfirmationAlert()
+ alert.props.title = _('Add userscript')
+ alert.props.msg = _('Do you want to add this userscript?')
+ alert.connect('response', self._userscript_found_response_cb)
+ self.add_alert(alert)
+
+ def _userscript_found_response_cb(self, alert, response_id):
+ self.remove_alert(alert)
+
+ if response_id is gtk.RESPONSE_OK:
+ pass
def _add_link(self):
''' take screenshot and add link info to the model '''
diff --git a/webtoolbar.py b/webtoolbar.py
index 428fd89..c71ce1a 100644
--- a/webtoolbar.py
+++ b/webtoolbar.py
@@ -16,6 +16,8 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from gettext import gettext as _
+import re
+import logging
import gobject
import gtk
@@ -23,12 +25,16 @@ import pango
from xpcom.components import interfaces
from xpcom import components
+from sugar.datastore import datastore
from sugar.graphics.toolbutton import ToolButton
from sugar.graphics.menuitem import MenuItem
+from sugar.graphics.alert import Alert
+from sugar.graphics.icon import Icon
from sugar._sugarext import AddressEntry
import filepicker
import places
+import ssb
_MAX_HISTORY_ENTRIES = 15
@@ -220,10 +226,11 @@ class WebToolbar(gtk.Toolbar):
([]))
}
- def __init__(self, browser):
+ def __init__(self, activity):
gtk.Toolbar.__init__(self)
- self._browser = browser
+ self._activity = activity
+ self._browser = activity._browser
self._loading = False
@@ -263,7 +270,7 @@ class WebToolbar(gtk.Toolbar):
self.insert(self._link_add, -1)
self._link_add.show()
- progress_listener = browser.progress
+ progress_listener = self._browser.progress
progress_listener.connect('location-changed',
self._location_changed_cb)
progress_listener.connect('loading-start', self._loading_start_cb)
@@ -276,6 +283,12 @@ class WebToolbar(gtk.Toolbar):
self._browser.connect("notify::title", self._title_changed_cb)
+ self._create_ssb = ToolButton('activity-ssb')
+ self._create_ssb.set_tooltip(_('Create SSB'))
+ self._create_ssb.connect('clicked', self._create_ssb_clicked_cb)
+ self.insert(self._create_ssb, -1)
+ self._create_ssb.show()
+
def _session_history_changed_cb(self, session_history, current_page_index):
# We have to wait until the history info is updated.
gobject.idle_add(self._reload_session_history, current_page_index)
@@ -396,3 +409,62 @@ class WebToolbar(gtk.Toolbar):
def _link_add_clicked_cb(self, button):
self.emit('add-link')
+ def _create_ssb_clicked_cb(self, button):
+ title = self._activity.webtitle
+ uri = self._activity.current
+ #favicon = self._activity.get_favicon()
+
+ pattern = re.compile(r'''
+ (\w+) # first word
+ [ _-]* # any amount and type of spacing
+ (\w+)? # second word, may be absent
+ ''', re.VERBOSE)
+ first, second = re.search(pattern, title).groups()
+
+ # CamelCase the two words
+ first = first.capitalize()
+ if second is not None:
+ second = second.capitalize()
+ name = first + ' ' + second
+ else:
+ name = first
+
+ self._ssb = ssb.SSBCreator(name, uri)
+
+ # alert to show after creation
+ alert = Alert()
+ alert.props.title = _('SSB Creation')
+
+ cancel_icon = Icon(icon_name='dialog-cancel')
+ alert.add_button(gtk.RESPONSE_CANCEL, _('Cancel'), cancel_icon)
+ cancel_icon.show()
+
+ open_icon = Icon(icon_name='filesave')
+ alert.add_button(gtk.RESPONSE_APPLY, _('Show in Journal'), open_icon)
+ open_icon.show()
+
+ ok_icon = Icon(icon_name='dialog-ok')
+ alert.add_button(gtk.RESPONSE_OK, _('Install'), ok_icon)
+ ok_icon.show()
+
+ self._activity.add_alert(alert)
+ alert.connect('response', self._create_ssb_alert_cb)
+
+ try:
+ self._ssb.create()
+ except Exception, e:
+ # DEBUG: alert shows exception message
+ alert.props.msg = _('Failed: ') + str(e)
+ else:
+ alert.props.msg = _('Done!')
+ finally:
+ alert.show()
+
+ def _create_ssb_alert_cb(self, alert, response_id):
+ self._activity.remove_alert(alert)
+
+ if response_id is not gtk.RESPONSE_CANCEL:
+ if response_id is gtk.RESPONSE_APPLY:
+ self._ssb.show_in_journal()
+ else:
+ self._ssb.install()