diff options
-rw-r--r-- | bookmarklets.py | 86 | ||||
-rw-r--r-- | bookmarklettoolbar.py | 95 | ||||
-rw-r--r-- | browser.py | 32 | ||||
-rw-r--r-- | edittoolbar.py | 50 | ||||
-rw-r--r-- | icons/activity-ssb.svg | 13 | ||||
-rw-r--r-- | icons/bookmarklet-inverted.svg | 6 | ||||
-rw-r--r-- | icons/bookmarklet-thick.svg | 6 | ||||
-rw-r--r-- | icons/bookmarklet.svg | 6 | ||||
-rw-r--r-- | palettes.py | 25 | ||||
-rw-r--r-- | ssb.py | 169 | ||||
-rw-r--r-- | usercode.py | 250 | ||||
-rw-r--r-- | webactivity.py | 119 | ||||
-rw-r--r-- | webtoolbar.py | 78 |
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 @@ -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): @@ -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() |