# 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 urlparse import urlparse from gettext import gettext as _ import gobject import gtk import pango import gtksourceview2 import xpcom from xpcom import components from xpcom.components import interfaces from sugar.activity import activity from sugar.graphics import style from sugar.graphics.icon import Icon SCRIPTS_PATH = os.path.join(activity.get_activity_root(), 'data/userscripts') STYLE_PATH = os.path.join(activity.get_activity_root(), 'data/style.user.css') # make sure the userscript dir exists if not os.path.isdir(SCRIPTS_PATH): os.mkdir(SCRIPTS_PATH) # make sure userstyle sheet exists open(STYLE_PATH, 'w').close() class Dialog(gtk.Window): def __init__(self, width=None, height=None): self.width = width or int(gtk.gdk.screen_width()/2) self.height = height or int(gtk.gdk.screen_height()/1.5) gtk.Window.__init__(self) self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) self.set_default_size(self.width, self.height) def show(self): self.show_all() gtk.Window.show(self) class SourceEditor(gtk.ScrolledWindow): __gtype_name__ = 'SugarSourceEditor' def __init__(self): gtk.ScrolledWindow.__init__(self) self.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC self.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC self._buffer = gtksourceview2.Buffer() self._buffer.set_highlight_syntax(True) self._source_view = gtksourceview2.View(self._buffer) self._source_view.set_editable(True) self._source_view.set_cursor_visible(True) self._source_view.set_show_line_numbers(True) self._source_view.set_show_right_margin(True) self._source_view.set_right_margin_position(80) #self._source_view.set_highlight_current_line(True) #FIXME: Ugly color self._source_view.modify_font(_SOURCE_FONT) self.add(self._source_view) self._source_view.show() self._file_path = None def get_text(self): start = self._buffer.get_start_iter() end = self._buffer.get_end_iter() return self._buffer.get_text(start, end) def set_text(self, text): self._buffer.set_text(text) text = property(get_text, set_text) def write(self, path=None): open(path or self.file_path, 'w').write(self.text) def _set_file_path(self, file_path): if file_path == self._file_path: return self._file_path = file_path if self._file_path is None: self._buffer.set_text('') return mime_type = mime.get_for_file(self._file_path) logging.debug('Detected mime type: %r' % mime_type) language_manager = gtksourceview2.language_manager_get_default() detected_language = None for language_id in language_manager.get_language_ids(): language = language_manager.get_language(language_id) if mime_type in language.get_mime_types(): detected_language = language break if detected_language is not None: logging.debug('Detected language: %r' % \ detected_language.get_name()) self._buffer.set_language(detected_language) self._buffer.set_text(open(self._file_path, 'r').read()) def _get_file_path(self): return self._file_path file_path = property(_get_file_path, _set_file_path) class ScriptFileViewer(gtk.ScrolledWindow): __gtype_name__ = 'SugarScriptFileViewer' __gsignals__ = { 'file-selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([str])), } def __init__(self, path): gtk.ScrolledWindow.__init__(self) self.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC self.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC self.set_size_request(style.GRID_CELL_SIZE * 3, -1) self._path = None ls = os.listdir(path) self._initial_filename = ls[0] if len(ls) > 0 else None self._tree_view = gtk.TreeView() self.add(self._tree_view) self._tree_view.show() self._tree_view.props.headers_visible = False selection = self._tree_view.get_selection() selection.connect('changed', self.__selection_changed_cb) cell = gtk.CellRendererText() column = gtk.TreeViewColumn() column.pack_start(cell, True) column.add_attribute(cell, 'text', 0) self._tree_view.append_column(column) self._tree_view.set_search_column(0) self.set_path(path) def set_path(self, path): self.emit('file-selected', None) if self._path == path: return self._path = path self._tree_view.set_model(gtk.TreeStore(str, str)) self._add_dir_to_model(path) def _add_dir_to_model(self, dir_path, parent=None): model = self._tree_view.get_model() for f in os.listdir(dir_path): if not f.endswith('.pyc'): full_path = os.path.join(dir_path, f) if os.path.isdir(full_path): new_iter = model.append(parent, [f, full_path]) self._add_dir_to_model(full_path, new_iter) else: current_iter = model.append(parent, [f, full_path]) if f == self._initial_filename: selection = self._tree_view.get_selection() selection.select_iter(current_iter) def __selection_changed_cb(self, selection): model, tree_iter = selection.get_selected() if tree_iter is None: file_path = None else: file_path = model.get_value(tree_iter, 1) self.emit('file-selected', file_path) def get_selected_file(self): selection = self._tree_view.get_selection() model, tree_iter = selection.get_selected() if tree_iter is None: return None else: return model.get_value(tree_iter, 1) def remove_file(self, file_path): model = self._tree_view.get_model() for i in model: if i[0] == os.path.basename(file_path): model.remove(model.get_iter(i.path)) break class StyleEditor(Dialog): __gsignals__ = { 'userstyle-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), } def __init__(self): Dialog.__init__(self) # layout vbox = gtk.VBox() self._editor = SourceEditor() self._editor.file_path = STYLE_PATH vbox.pack_start(self._editor) # buttons buttonbox = gtk.HBox() 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')) self._save_button.connect('clicked', self.__save_button_cb) buttonbox.pack_start(self._save_button) vbox.pack_start(buttonbox, expand=False) self.add(vbox) def __save_button_cb(self, button): self._editor.write() self.emit('userstyle-changed') self.destroy() def __cancel_button_cb(self, button): self.destroy() class ScriptEditor(Dialog): def __init__(self): Dialog.__init__(self) # layout hbox = gtk.HBox() self._fileview = ScriptFileViewer(SCRIPTS_PATH) self._fileview.connect('file-selected', self.__file_selected_cb) hbox.pack_start(self._fileview, expand=False) editbox = gtk.VBox() self._editor = SourceEditor() editbox.pack_start(self._editor) buttonbox = gtk.HBox() self._cancel_button = gtk.Button(label=_('Close')) 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._delete_button = gtk.Button(label=_('Delete')) self._delete_button.set_image(Icon(icon_name='stock_delete')) self._delete_button.connect('clicked', self.__delete_button_cb) buttonbox.pack_start(self._delete_button) self._save_button = gtk.Button(label=_('Save')) self._save_button.set_image(Icon(icon_name='dialog-ok')) self._save_button.connect('clicked', self.__save_button_cb) buttonbox.pack_start(self._save_button) editbox.pack_start(buttonbox, expand=False) hbox.pack_start(editbox) self.add(hbox) self.__file_selected_cb(self._fileview, self._fileview._initial_filename) def __save_button_cb(self, button): self._editor.write() def __delete_button_cb(self, button): file_path = self._fileview.get_selected_file() self._fileview.remove_file(file_path) os.remove(file_path) def __cancel_button_cb(self, button): self.destroy() def __file_selected_cb(self, view, file_path): self._editor.file_path = self._fileview.get_selected_file() def add_script(location): cls = components.classes[ \ '@mozilla.org/embedding/browser/nsWebBrowserPersist;1'] persist = cls.createInstance(interfaces.nsIWebBrowserPersist) persist.persistFlags = interfaces.nsIWebBrowserPersist \ .PERSIST_FLAGS_REPLACE_EXISTING_FILES cls = components.classes["@mozilla.org/network/io-service;1"] uri = cls.getService(interfaces.nsIIOService).newURI(location, None, None) cls = components.classes["@mozilla.org/file/local;1"] local_file = cls.createInstance(interfaces.nsILocalFile) file_name = os.path.basename(uri.path) file_path = os.path.join(SCRIPTS_PATH, file_name) local_file.initWithPath(file_path) if not local_file.exists(): local_file.create(0x00, 0644) logging.debug('Saving userscript %s -> %s' % \ (uri.spec, file_path)) persist.saveURI(uri, None, None, None, None, local_file) def script_exists(location): script_name = os.path.basename(urlparse(location).path) return os.path.isfile(os.path.join(SCRIPTS_PATH, script_name)) class Injector(): _com_interfaces_ = interfaces.nsIDOMEventListener def __init__(self, script_path): self.script_path = script_path self._wrapped = xpcom.server.WrapObject(self, interfaces.nsIDOMEventListener) def handleEvent(self, event): self.head.appendChild(self.script) def attach_to(self, window): # set up the script element to be injected self.script = window.document.createElement('script') self.script.type = 'text/javascript' # work around XSS security text = str(open(self.script_path,'r').read()) self.script.appendChild( window.document.createTextNode(text) ) # reference to head self.head = window.document.getElementsByTagName('head').item(0) # actual attaching window.addEventListener('load', self._wrapped, False) class ScriptListener(gobject.GObject): _com_interfaces_ = interfaces.nsIWebProgressListener __gsignals__ = { 'userscript-found': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([str])), 'userscript-inject': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([str])), } def __init__(self): gobject.GObject.__init__(self) self._wrapped = xpcom.server.WrapObject( \ self, interfaces.nsIWebProgressListener) def onLocationChange(self, webProgress, request, location): if location.spec.endswith('.user.js'): self.emit('userscript-found', location.spec) else: # TODO load scripts according to domain regex for i in os.listdir(SCRIPTS_PATH): script_path = os.path.join(SCRIPTS_PATH, i) self.emit('userscript-inject', script_path) def setup(self, browser): browser.web_progress.addProgressListener(self._wrapped, interfaces.nsIWebProgress.NOTIFY_LOCATION)