Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--agent-stylesheet.css8
-rw-r--r--browser.dtd21
-rw-r--r--clickToView.xml239
-rw-r--r--downloadmanager.py273
-rw-r--r--filepicker.py146
-rw-r--r--linkbutton.py102
-rw-r--r--messenger.py126
-rw-r--r--model.py79
-rw-r--r--po/pseudo.po101
-rw-r--r--progresslistener.py96
-rw-r--r--promptservice.py68
-rw-r--r--sessionstore.py69
-rw-r--r--webactivity.py460
-rw-r--r--webtoolbar.py217
14 files changed, 2005 insertions, 0 deletions
diff --git a/agent-stylesheet.css b/agent-stylesheet.css
new file mode 100644
index 0000000..6c94d0e
--- /dev/null
+++ b/agent-stylesheet.css
@@ -0,0 +1,8 @@
+/* Prevent flash animations from playing until you click on them. */
+object[classid$=":D27CDB6E-AE6D-11cf-96B8-444553540000"],
+object[codebase*="swflash.cab"],
+object[type="application/x-shockwave-flash"],
+embed[type="application/x-shockwave-flash"],
+embed[src$=".swf"]
+{ -moz-binding: url("clickToView.xml#flash"); }
+
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/clickToView.xml b/clickToView.xml
new file mode 100644
index 0000000..6164c73
--- /dev/null
+++ b/clickToView.xml
@@ -0,0 +1,239 @@
+<?xml version="1.0"?>
+<!-- This provides a click-to-view capability to block flash
+ and java content in Firefox. Derived from:
+ http://www.squarefree.com/userstyles/xbl.html
+ and modified to allow different messages for flash and java
+ by Steve Zobell. This can be placed in the rez directory in
+ the Firefox install (e.g. C:\Program Files\Mozilla Firefox\res).
+
+ The blocking capabilities are derived from this description:
+ http://www.wlug.org.nz/MozillaNotes
+
+ Add the following to userContent.css (in the chrome directory
+ in the user profile):
+
+/* Prevent flash animations from playing until you click on them. */
+object[classid$=":D27CDB6E-AE6D-11cf-96B8-444553540000"],
+object[codebase*="swflash.cab"],
+object[type="application/x-shockwave-flash"],
+embed[type="application/x-shockwave-flash"],
+embed[src$=".swf"]
+{ -moz-binding: url("resource:///res/clickToView.xml#flash"); }
+
+/* Block java, with a click to load feature. */
+object[codebase*="java"],
+object[type="application/java"],
+embed[type="application/java"],
+applet[code$=".class"]
+{ -moz-binding: url("resource:///res/clickToView.xml#java"); }
+
+-->
+<bindings xmlns="http://www.mozilla.org/xbl" xmlns:html="http://www.w3.org/1999/xhtml">
+
+<binding id="flash">
+ <implementation>
+ <constructor>
+ <![CDATA[
+
+
+var doomed = this;
+
+// Check parent too (case of working EMBED inside broken OBJECT)
+if (doomed.overrideCTV || doomed.parentNode.overrideCTV)
+ return;
+doomed.overrideCTV = true;
+
+var placeholder = document.createElement("div");
+
+
+// === Styling ===
+
+var w = parseInt(doomed.width || doomed.style.width);
+var h = parseInt(doomed.height || doomed.style.height);
+
+if (!w || w < 32) w = 32; // !w catches e.g. NaN
+if (!h || h < 32) h = 32;
+
+placeholder.style.width = w + "px";
+placeholder.style.height = h + "px";
+placeholder.style.backgroundColor = "yellow";
+placeholder.style.MozOutline = "1px solid red";
+placeholder.style.textAlign = "center";
+placeholder.style.color = "black";
+placeholder.style.background = "white";
+placeholder.style.opacity = 0.4;
+placeholder.style.cursor = "pointer";
+placeholder.style.overflow = "hidden";
+
+var red = document.createElement("span");
+red.style.fontWeight = "bold";
+red.style.fontStyle = "italic";
+red.style.color = "red";
+red.appendChild(document.createTextNode("Flash"));
+
+var text = document.createTextNode(" [[Click to play]] ");
+
+
+// A button to dismiss the placeholder (instead of playing)
+// Useful for those wonderful flash animations that cover
+// the text on the page at the start.
+var bye = document.createElement("bye");
+bye.style.backgroundColor = "white";
+bye.style.MozOutline = "2px solid red";
+bye.style.textAlign = "center";
+bye.style.color = "red";
+bye.style.background = "white";
+bye.style.cursor = "pointer";
+bye.style.overflow = "hidden";
+bye.appendChild(document.createTextNode("X"));
+
+placeholder.appendChild(red);
+placeholder.appendChild(text);
+placeholder.appendChild(bye);
+
+placeholder.onmouseover = function()
+{
+ this.style.MozOutline='3px outset white';
+ this.style.opacity = 1.0;
+ this.style.background='lightgrey'
+}
+placeholder.onmouseout = function()
+{
+ this.style.background='white';
+ this.style.opacity = 0.4;
+ this.style.MozOutline='1px solid red';
+}
+
+
+// === Replacement ===
+
+var p = doomed.parentNode;
+
+// Do in a timeout to avoid bugginess
+setTimeout ( function ()
+{
+ p.insertBefore(placeholder, doomed);
+ p.removeChild(doomed);
+}, 0);
+
+// I love closures
+placeholder.onclick = function()
+{
+ p.insertBefore(doomed, placeholder);
+ p.removeChild(placeholder);
+}
+
+
+bye.onclick = function()
+{
+ p.removeChild(placeholder);
+}
+
+ ]]>
+ </constructor>
+ </implementation>
+</binding>
+
+
+<binding id="java">
+ <implementation>
+ <constructor>
+ <![CDATA[
+
+
+var doomed = this;
+
+// Check parent too (case of working EMBED inside broken OBJECT)
+if (doomed.overrideCTV || doomed.parentNode.overrideCTV)
+ return;
+doomed.overrideCTV = true;
+
+var placeholder = document.createElement("div");
+
+
+// === Styling ===
+
+var w = parseInt(doomed.width || doomed.style.width);
+var h = parseInt(doomed.height || doomed.style.height);
+
+if (!w || w < 32) w = 32; // !w catches e.g. NaN
+if (!h || h < 32) h = 32;
+
+placeholder.style.width = w + "px";
+placeholder.style.height = h + "px";
+placeholder.style.backgroundColor = "yellow";
+placeholder.style.MozOutline = "1px solid red";
+placeholder.style.textAlign = "center";
+placeholder.style.color = "black";
+placeholder.style.background = "white";
+placeholder.style.opacity = 0.4;
+placeholder.style.cursor = "pointer";
+placeholder.style.overflow = "hidden";
+
+var red = document.createElement("span");
+red.style.fontWeight = "bold";
+red.style.fontStyle = "italic";
+red.style.color = "red";
+red.appendChild(document.createTextNode("Java"))
+
+var text = document.createTextNode(" [[Click to play]] ");
+
+placeholder.appendChild(red);
+placeholder.appendChild(text);
+
+// A button to dismiss the placeholder (instead of playing)
+var bye = document.createElement("span");
+bye.style.backgroundColor = "white";
+bye.style.MozOutline = "2px solid red";
+bye.style.textAlign = "center";
+bye.style.color = "red";
+bye.style.background = "white";
+bye.style.cursor = "pointer";
+bye.style.overflow = "hidden";
+bye.appendChild(document.createTextNode("X"))
+placeholder.appendChild(bye);
+
+placeholder.onmouseover = function()
+{
+ this.style.MozOutline='3px outset white';
+ this.style.opacity = 1.0;
+ this.style.background='lightgrey'
+}
+placeholder.onmouseout = function()
+{
+ this.style.background='white';
+ this.style.opacity = 0.4;
+ this.style.MozOutline='1px solid red';
+}
+
+
+// === Replacement ===
+
+var p = doomed.parentNode;
+
+// Do in a timeout to avoid bugginess
+setTimeout ( function ()
+{
+ p.insertBefore(placeholder, doomed);
+ p.removeChild(doomed);
+}, 0);
+
+// I love closures
+placeholder.onclick = function()
+{
+ p.insertBefore(doomed, placeholder);
+ p.removeChild(placeholder);
+}
+
+
+bye.onclick = function()
+{
+ p.removeChild(placeholder);
+}
+
+ ]]>
+ </constructor>
+ </implementation>
+</binding>
+
+</bindings>
diff --git a/downloadmanager.py b/downloadmanager.py
new file mode 100644
index 0000000..156da0b
--- /dev/null
+++ b/downloadmanager.py
@@ -0,0 +1,273 @@
+# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import logging
+from gettext import gettext as _
+import time
+import gtk
+
+from xpcom.nsError import *
+from xpcom import components
+from xpcom.components import interfaces
+from xpcom.server.factory import Factory
+import dbus
+
+from sugar.datastore import datastore
+from sugar import profile
+from sugar import mime
+from sugar.graphics.alert import Alert, TimeoutAlert
+from sugar.graphics.icon import Icon
+from sugar.activity import activity
+
+# #3903 - this constant can be removed and assumed to be 1 when dbus-python
+# 0.82.3 is the only version used
+import dbus
+if dbus.version >= (0, 82, 3):
+ DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1
+else:
+ DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND = 1000
+
+NS_BINDING_ABORTED = 0x804b0002 # From nsNetError.h
+
+DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
+DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
+DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
+
+_MIN_TIME_UPDATE = 5 # In seconds
+_MIN_PERCENT_UPDATE = 10
+
+_browser = None
+_activity = None
+_temp_path = '/tmp'
+def init(browser, activity, temp_path):
+ global _browser
+ _browser = browser
+
+ global _activity
+ _activity = activity
+
+ global _temp_path
+ _temp_path = temp_path
+
+_active_downloads = []
+
+def can_quit():
+ return len(_active_downloads) == 0
+
+def remove_all_downloads():
+ for download in _active_downloads:
+ download._cancelable.cancel(NS_ERROR_FAILURE)
+ if download._dl_jobject is not None:
+ download._datastore_deleted_handler.remove()
+ datastore.delete(download._dl_jobject.object_id)
+ download._cleanup_datastore_write()
+
+class DownloadManager:
+ _com_interfaces_ = interfaces.nsIHelperAppLauncherDialog
+
+ def promptForSaveToFile(self, launcher, window_context,
+ default_file, suggested_file_extension):
+ file_class = components.classes["@mozilla.org/file/local;1"]
+ dest_file = file_class.createInstance(interfaces.nsILocalFile)
+
+ if not default_file:
+ default_file = time.time()
+ if suggested_file_extension:
+ default_file = '%s.%s' % (default_file, suggested_file_extension)
+
+ global _temp_path
+ if not os.path.exists(_temp_path):
+ os.makedirs(_temp_path)
+ file_path = os.path.join(_temp_path, default_file)
+
+ print file_path
+ dest_file.initWithPath(file_path)
+
+ return dest_file
+
+ def show(self, launcher, context, reason):
+ launcher.saveToDisk(None, False)
+ return NS_OK
+
+components.registrar.registerFactory('{64355793-988d-40a5-ba8e-fcde78cac631}"',
+ 'Sugar Download Manager',
+ '@mozilla.org/helperapplauncherdialog;1',
+ Factory(DownloadManager))
+
+class Download:
+ _com_interfaces_ = interfaces.nsITransfer
+
+ def init(self, source, target, display_name, mime_info, start_time,
+ temp_file, cancelable):
+ self._source = source
+ self._mime_type = mime_info.MIMEType
+ self._temp_file = temp_file
+ self._target_file = target.queryInterface(interfaces.nsIFileURL).file
+ self._cancelable = cancelable
+
+ self._dl_jobject = None
+ self._object_id = None
+ self._last_update_time = 0
+ self._last_update_percent = 0
+ self._stop_alert = None
+
+ return NS_OK
+
+ def onStatusChange(self, web_progress, request, status, message):
+ logging.info('Download.onStatusChange(%r, %r, %r, %r)' % \
+ (web_progress, request, status, message))
+
+ def onStateChange(self, web_progress, request, state_flags, status):
+ if state_flags == interfaces.nsIWebProgressListener.STATE_START:
+ self._create_journal_object()
+ self._object_id = self._dl_jobject.object_id
+
+ alert = TimeoutAlert(9)
+ alert.props.title = _('Download started')
+ path, file_name = os.path.split(self._target_file.path)
+ alert.props.msg = _('%s'%(file_name))
+ _activity.add_alert(alert)
+ alert.connect('response', self.__start_response_cb)
+ alert.show()
+ global _active_downloads
+ _active_downloads.append(self)
+
+ elif state_flags == interfaces.nsIWebProgressListener.STATE_STOP:
+ if NS_FAILED(status): # download cancelled
+ return
+
+ self._stop_alert = Alert()
+ self._stop_alert.props.title = _('Download completed')
+ path, file_name = os.path.split(self._target_file.path)
+ self._stop_alert.props.msg = _('%s'%(file_name))
+ open_icon = Icon(icon_name='zoom-activity')
+ self._stop_alert.add_button(gtk.RESPONSE_APPLY, _('Open'), open_icon)
+ open_icon.show()
+ ok_icon = Icon(icon_name='dialog-ok')
+ self._stop_alert.add_button(gtk.RESPONSE_OK, _('Ok'), ok_icon)
+ ok_icon.show()
+ _activity.add_alert(self._stop_alert)
+ self._stop_alert.connect('response', self.__stop_response_cb)
+ self._stop_alert.show()
+
+ self._dl_jobject.metadata['title'] = _('File %s from %s.') % \
+ (file_name, self._source.spec)
+ self._dl_jobject.metadata['progress'] = '100'
+ self._dl_jobject.file_path = self._target_file.path
+
+ if self._mime_type == 'application/octet-stream':
+ sniffed_mime_type = mime.get_for_file(self._target_file.path)
+ self._dl_jobject.metadata['mime_type'] = sniffed_mime_type
+
+ datastore.write(self._dl_jobject,
+ transfer_ownership=True,
+ reply_handler=self._internal_save_cb,
+ error_handler=self._internal_save_error_cb,
+ timeout=360 * DBUS_PYTHON_TIMEOUT_UNITS_PER_SECOND)
+
+ def __start_response_cb(self, alert, response_id):
+ global _active_downloads
+ if response_id is gtk.RESPONSE_CANCEL:
+ logging.debug('Download Canceled')
+ self._cancelable.cancel(NS_ERROR_FAILURE)
+ try:
+ self._datastore_deleted_handler.remove()
+ datastore.delete(self._object_id)
+ except:
+ logging.warning('Object has been deleted already')
+ if self._dl_jobject is not None:
+ self._cleanup_datastore_write()
+ if self._stop_alert is not None:
+ _activity.remove_alert(self._stop_alert)
+
+ _activity.remove_alert(alert)
+
+ def __stop_response_cb(self, alert, response_id):
+ global _active_downloads
+ if response_id is gtk.RESPONSE_APPLY:
+ logging.debug('Start application with downloaded object')
+ activity.show_object_in_journal(self._object_id)
+ _activity.remove_alert(alert)
+
+ def _cleanup_datastore_write(self):
+ global _active_downloads
+ _active_downloads.remove(self)
+
+ if os.path.isfile(self._dl_jobject.file_path):
+ os.remove(self._dl_jobject.file_path)
+ self._dl_jobject.destroy()
+ self._dl_jobject = None
+
+ def _internal_save_cb(self):
+ self._cleanup_datastore_write()
+
+ def _internal_save_error_cb(self, err):
+ logging.debug("Error saving activity object to datastore: %s" % err)
+ self._cleanup_datastore_write()
+
+ def onProgressChange64(self, web_progress, request, cur_self_progress,
+ max_self_progress, cur_total_progress,
+ max_total_progress):
+ path, file_name = os.path.split(self._target_file.path)
+ percent = (cur_self_progress * 100) / max_self_progress
+
+ if (time.time() - self._last_update_time) < _MIN_TIME_UPDATE and \
+ (percent - self._last_update_percent) < _MIN_PERCENT_UPDATE:
+ return
+
+ self._last_update_time = time.time()
+ self._last_update_percent = percent
+
+ if percent < 100:
+ self._dl_jobject.metadata['progress'] = str(percent)
+ datastore.write(self._dl_jobject)
+
+ def _create_journal_object(self):
+ path, file_name = os.path.split(self._target_file.path)
+
+ self._dl_jobject = datastore.create()
+ self._dl_jobject.metadata['title'] = _('Downloading %s from \n%s.') \
+ %(file_name, self._source.spec)
+
+ self._dl_jobject.metadata['progress'] = '0'
+ self._dl_jobject.metadata['keep'] = '0'
+ self._dl_jobject.metadata['buddies'] = ''
+ self._dl_jobject.metadata['preview'] = ''
+ self._dl_jobject.metadata['icon-color'] = profile.get_color().to_string()
+ self._dl_jobject.metadata['mime_type'] = self._mime_type
+ self._dl_jobject.file_path = ''
+ datastore.write(self._dl_jobject)
+
+ bus = dbus.SessionBus()
+ obj = bus.get_object(DS_DBUS_SERVICE, DS_DBUS_PATH)
+ datastore_dbus = dbus.Interface(obj, DS_DBUS_INTERFACE)
+ self._datastore_deleted_handler = datastore_dbus.connect_to_signal(
+ 'Deleted', self.__datastore_deleted_cb,
+ arg0=self._dl_jobject.object_id)
+
+ def __datastore_deleted_cb(self, uid):
+ logging.debug('Downloaded entry has been deleted from the datastore: %r' % uid)
+ # TODO: Use NS_BINDING_ABORTED instead of NS_ERROR_FAILURE.
+ self._cancelable.cancel(NS_ERROR_FAILURE) #NS_BINDING_ABORTED)
+ global _active_downloads
+ _active_downloads.remove(self)
+
+components.registrar.registerFactory('{23c51569-e9a1-4a92-adeb-3723db82ef7c}"',
+ 'Sugar Download',
+ '@mozilla.org/transfer;1',
+ Factory(Download))
+
diff --git a/filepicker.py b/filepicker.py
new file mode 100644
index 0000000..5b24c7c
--- /dev/null
+++ b/filepicker.py
@@ -0,0 +1,146 @@
+# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+import os
+import tempfile
+import shutil
+
+import gtk
+
+import xpcom
+from xpcom import components
+from xpcom.components import interfaces
+from xpcom.server.factory import Factory
+
+from sugar.graphics.objectchooser import ObjectChooser
+
+_temp_files_to_clean = []
+
+def cleanup_temp_files():
+ for temp_file in _temp_files_to_clean:
+ logging.debug('filepicker.cleanup_temp_files: %r' % temp_file)
+ os.remove(temp_file)
+
+class FilePicker:
+ _com_interfaces_ = interfaces.nsIFilePicker
+
+ cid = '{57901c41-06cb-4b9e-8258-37323327b583}'
+ description = 'Sugar File Picker'
+
+ def __init__(self):
+ self._title = None
+ self._parent = None
+ self._file = None
+
+ def appendFilter(self, title, filter):
+ logging.warning('FilePicker.appendFilter: UNIMPLEMENTED')
+
+ def appendFilters(self, filterMask):
+ logging.warning('FilePicker.appendFilters: UNIMPLEMENTED')
+
+ def init(self, parent, title, mode):
+ self._title = title
+ self._file = None
+ """
+ cls = components.classes['@mozilla.org/embedcomp/window-watcher;1']
+ window_watcher = cls.getService(interfaces.nsIWindowWatcher)
+ chrome = window_watcher.getChromeForWindow(parent)
+ self._parent = chrome.web_view.get_toplevel()
+ """
+ self._parent = None
+
+ if mode != interfaces.nsIFilePicker.modeOpen:
+ raise xpcom.COMException(NS_ERROR_NOT_IMPLEMENTED)
+
+ def show(self):
+ chooser = ObjectChooser(self._title, self._parent,
+ gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
+ try:
+ result = chooser.run()
+ if result == gtk.RESPONSE_ACCEPT:
+ logging.debug('FilePicker.show: %r' % chooser.get_selected_object())
+ jobject = chooser.get_selected_object()
+ if jobject and jobject.file_path:
+ ext = os.path.splitext(jobject.file_path)[1]
+ f, new_temp = tempfile.mkstemp(ext)
+ del f
+
+ global _temp_files_to_clean
+ _temp_files_to_clean.append(new_temp)
+ shutil.copy(jobject.file_path, new_temp)
+
+ self._file = new_temp
+ finally:
+ chooser.destroy()
+ del chooser
+
+ if self._file:
+ return interfaces.nsIFilePicker.returnOK
+ else:
+ return interfaces.nsIFilePicker.returnCancel
+
+ def set_defaultExtension(self, default_extension):
+ logging.warning('FilePicker.set_defaultExtension: UNIMPLEMENTED')
+
+ def get_defaultExtension(self):
+ logging.warning('FilePicker.get_defaultExtension: UNIMPLEMENTED')
+ return None
+
+ def set_defaultString(self, default_string):
+ logging.warning('FilePicker.set_defaultString: UNIMPLEMENTED')
+
+ def get_defaultString(self):
+ logging.warning('FilePicker.get_defaultString: UNIMPLEMENTED')
+ return None
+
+ def set_displayDirectory(self, display_directory):
+ logging.warning('FilePicker.set_displayDirectory: UNIMPLEMENTED')
+
+ def get_displayDirectory(self):
+ logging.warning('FilePicker.get_displayDirectory: UNIMPLEMENTED')
+ return None
+
+ def set_filterIndex(self, filter_index):
+ logging.warning('FilePicker.set_filterIndex: UNIMPLEMENTED')
+
+ def get_filterIndex(self):
+ logging.warning('FilePicker.get_filterIndex: UNIMPLEMENTED')
+ return None
+
+ def get_file(self):
+ logging.debug('FilePicker.get_file: %r' % self._file)
+ if self._file:
+ cls = components.classes["@mozilla.org/file/local;1"]
+ local_file = cls.createInstance(interfaces.nsILocalFile)
+ local_file.initWithPath(self._file)
+ return local_file
+ else:
+ return None
+
+ def get_Files(self):
+ logging.warning('FilePicker.get_Files: UNIMPLEMENTED')
+ return None
+
+ def get_FileURL(self):
+ logging.warning('FilePicker.get_FileURL: UNIMPLEMENTED')
+ return None
+
+components.registrar.registerFactory(FilePicker.cid,
+ FilePicker.description,
+ '@mozilla.org/filepicker;1',
+ Factory(FilePicker))
+
diff --git a/linkbutton.py b/linkbutton.py
new file mode 100644
index 0000000..b1c8c8d
--- /dev/null
+++ b/linkbutton.py
@@ -0,0 +1,102 @@
+# 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 gobject
+from gettext import gettext as _
+import rsvg
+import re
+import gc
+import pango
+
+from sugar.graphics.palette import Palette
+from sugar.graphics.tray import TrayButton
+from sugar.graphics.icon import Icon
+from sugar.graphics import style
+
+
+class LinkButton(TrayButton, gobject.GObject):
+ __gtype_name__ = 'LinkButton'
+ __gsignals__ = {
+ 'remove_link': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([str]))
+ }
+ def __init__(self, url, buffer, color, title, owner, index, hash):
+ TrayButton.__init__(self)
+ self.set_image(buffer, color.split(',')[1], color.split(',')[0])
+
+ self.hash = hash
+ info = title +'\n'+ owner
+ self.setup_rollover_options(info)
+
+ 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/link.svg")
+ pixbuf_bg = self._read_link_background(xo_buddy, fill, stroke)
+ pixbuf_bg = pixbuf_bg.scale_simple(style.zoom(120),
+ style.zoom(110),
+ gtk.gdk.INTERP_BILINEAR)
+ dest_x = style.zoom(10)
+ dest_y = style.zoom(20)
+ w = pixbuf.get_width()
+ h = pixbuf.get_height()
+ scale_x = 1
+ scale_y = 1
+
+ pixbuf.composite(pixbuf_bg, dest_x, dest_y, w, h, dest_x, dest_y,
+ scale_x, scale_y, gtk.gdk.INTERP_BILINEAR, 255)
+ img.set_from_pixbuf(pixbuf_bg)
+ self.set_icon_widget(img)
+ img.show()
+ del pixbuf
+ del pixbuf_bg
+ gc.collect()
+
+ def _read_link_background(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 setup_rollover_options(self, info):
+ palette = Palette(info, text_maxlen=50)
+ self.set_palette(palette)
+
+ menu_item = gtk.MenuItem(_('Remove'))
+ menu_item.connect('activate', self.item_remove_cb)
+ palette.menu.append(menu_item)
+ menu_item.show()
+
+ def item_remove_cb(self, widget):
+ self.emit('remove_link', self.hash)
diff --git a/messenger.py b/messenger.py
new file mode 100644
index 0000000..92e2869
--- /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
+
+SERVICE = "org.laptop.WebActivity"
+IFACE = SERVICE
+PATH = "/org/laptop/WebActivity"
+
+_logger = logging.getLogger('messenger')
+
+class Messenger(ExportedGObject):
+ def __init__(self, tube, is_initiator, model):
+ ExportedGObject.__init__(self, tube, PATH)
+ self.tube = tube
+ self.is_initiator = is_initiator
+ self.members = []
+ self.entered = False
+ self.model = model
+ 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 in removed:
+ _logger.debug('Remove member %r', handle)
+ try:
+ self.members.remove(self.tube.participants[handle])
+ 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)
+ self.bus_name = self.tube.get_unique_name()
+ if self.is_initiator:
+ _logger.debug('Initialising a new shared browser, I am %s .'
+ %self.tube.get_unique_name())
+ else:
+ # sync with other members
+ _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, sender):
+ a_ids.pop()
+ for link in self.model.data['shared_links']:
+ if link['hash'] not in a_ids:
+ self.tube.get_object(sender, PATH).send_link(
+ link['hash'], link['url'], link['title'], link['color'],
+ link['owner'], link['thumb'], link['timestamp'])
+
+ def error_sync(self, e, when):
+ _logger.error('Error %s: %s'%(when, e))
+
+ @dbus.service.method(dbus_interface=IFACE, in_signature='as',
+ out_signature='ass', 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.data['shared_links']:
+ if link['hash'] not in b_ids:
+ self.tube.get_object(sender, PATH).send_link(
+ link['hash'], link['url'], link['title'], link['color'],
+ link['owner'], link['thumb'], link['timestamp'])
+ a_ids = self.model.get_links_ids()
+ a_ids.append('')
+ # links I want from the caller
+ return (a_ids, self.bus_name)
+
+ @dbus.service.method(dbus_interface=IFACE, in_signature='ssssssd', out_signature='')
+ def send_link(self, id, url, title, color, owner, buffer, timestamp):
+ '''Send link'''
+ a_ids = self.model.get_links_ids()
+ if id not in a_ids:
+ thumb = base64.b64decode(buffer)
+ self.model.add_link(url, title, thumb, owner, color, timestamp)
+
+ @dbus.service.signal(IFACE, signature='sssssd')
+ def _add_link(self, url, title, color, owner, thumb, timestamp):
+ '''Signal to send the link information (add)'''
+ _logger.debug('Add Link: %s '%url)
+
+ def _add_link_receiver(self, url, title, color, owner, buffer, timestamp, sender=None):
+ '''Member sent a link'''
+ handle = self.tube.bus_name_to_handle[sender]
+ if self.tube.self_handle != handle:
+ thumb = base64.b64decode(buffer)
+ self.model.add_link(url, title, thumb, owner, color, timestamp)
+ _logger.debug('Added link: %s to linkbar.'%(url))
+
diff --git a/model.py b/model.py
new file mode 100644
index 0000000..ae2d505
--- /dev/null
+++ b/model.py
@@ -0,0 +1,79 @@
+#
+# 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 os
+import json
+import sha
+import gobject
+import base64
+
+class Model(gobject.GObject):
+ ''' The model of web-activity which uses json to serialize its data
+ to a file and deserealize from it.
+ '''
+ __gsignals__ = {
+ 'add_link': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ([int]))
+ }
+
+ def __init__(self):
+ gobject.GObject.__init__(self)
+ self.data = {}
+ self.data['shared_links'] = []
+ self.data['deleted'] = []
+
+ def add_link(self, url, title, thumb, owner, color, timestamp):
+ index = len(self.data['shared_links'])
+ for item in self.data['shared_links']:
+ if timestamp <= item['timestamp']:
+ index = self.data['shared_links'].index(item)
+ break
+
+ self.data['shared_links'].insert(index,
+ {'hash':sha.new(str(url)).hexdigest(),
+ 'url':str(url), 'title':str(title),
+ 'thumb':base64.b64encode(thumb),
+ 'owner':str(owner), 'color':str(color),
+ 'timestamp':float(timestamp)} )
+ self.emit('add_link', index)
+
+ def remove_link(self, hash):
+ for link in self.data['shared_links']:
+ if link['hash'] == hash:
+ self.data['deleted'].append(link['hash'])
+ self.data['shared_links'].remove(link)
+ break
+
+ def serialize(self):
+ return json.write(self.data)
+
+ def deserialize(self, data):
+ self.data = json.read(data)
+ if not self.data.has_key('shared_links'):
+ self.data['shared_links'] = []
+ if not self.data.has_key('deleted'):
+ self.data['deleted'] = []
+
+ def get_links_ids(self):
+ ids = []
+ for link in self.data['shared_links']:
+ ids.append(link['hash'])
+ ids.extend(self.data['deleted'])
+ ids.append('')
+ return ids
+
diff --git a/po/pseudo.po b/po/pseudo.po
new file mode 100644
index 0000000..13193b3
--- /dev/null
+++ b/po/pseudo.po
@@ -0,0 +1,101 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2007-11-01 22:55+0100\n"
+"PO-Revision-Date: 2007-11-21 03:27+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: activity/activity.info:2 webactivity.py:83
+msgid "Browse"
+msgstr "[ҳﯽ Сیपżل Ƶ Հҟ]Вҕօաیэ"
+
+#: browser.py:133
+msgid "Source"
+msgstr "[ոڃ юխ ڧբ Њ а ]Տօٮҕҫչ"
+
+#: downloadmanager.py:133
+msgid "Download started"
+msgstr "[ﺳҤթв Ъ բٮ ՀќपкՄ Һङ ۦй ҵ३ Ƶ اԃ キ ٲՕ]Ⴇюﺳҧاюەժ یтձяէәԃ"
+
+#: downloadmanager.py:135 downloadmanager.py:149
+#, python-format
+msgid "%s"
+msgstr "[چﻻՍЯ]%s"
+
+#: downloadmanager.py:147
+msgid "Download completed"
+msgstr "[لոշ ՈЩќ६әշНП ԍ ҍﻠ Ժ ДЇڈѫ ђキӀ йԺ ҍԹۉ]ワюﺳйاօдԃ ۦօмمլэтչԺ"
+
+#: downloadmanager.py:151
+msgid "Open"
+msgstr "[لЍ Ҏ Ёհ ў ]Юمչռ"
+
+#: downloadmanager.py:154
+msgid "Ok"
+msgstr "[սФ ۉ]Өҟ"
+
+#: downloadmanager.py:162
+#, python-format
+msgid "File %s downloaded from\n%s."
+msgstr "[լ ӨղҪ Գی モسԃ ﻠРщձ СچжҕҏƵҴеզъՅդ ճЦФЌ ӨƵ Ъѓ фՑ اч ]Բآլэ %s Ժօաиլօаժڿԃ ғҕюм\n%s."
+
+#: downloadmanager.py:250
+#, python-format
+msgid "Downloading %s from \n%s."
+msgstr "[գƶԲգ օ u キՄ ҎО ЩԳ گՕ Пۦ ш б ەդօ ђمƶ ҏ օ४й ﺫاЪ]ワюաиӀօەԃւղڧ %s քѓюм \n%s."
+
+#: linkbutton.py:95
+msgid "Remove"
+msgstr "[սЖ Ƶ Ⴇ щﺳЙ ڃӨ ]Яємюմє"
+
+#: webactivity.py:91
+msgid "View"
+msgstr "[ﻨ ЦҺ Յ ш]Ѵւэш"
+
+#: webactivity.py:109 webactivity.py:110
+msgid "blank"
+msgstr "[ВяЮШՉԺ ҧЙ]ҍاձйќ"
+
+#: webactivity.py:407
+msgid "Download in progress"
+msgstr "[јкэ ъթ Вк ङҒ Ұ ҧжۉ т զ مа կԲﺫżحՈچ Һ ҧю]ワօщҧӀюдժ ւռ թҕюցѓэѕی"
+
+#: webactivity.py:408
+msgid "Stopping now will cancel your download"
+msgstr "[Мأ Ц բЯ४Пﺳ МԴкҬ Ңи Н ﺫքթҢ ﻨ бӴԃ ӞчҲ ҕ Ҫո Ќ ѓФթ اՑՑҘӴ яЈѕ Ⴇ ङ ڿ]Ѕէօمթآҧց ոюﺳ աїاӀ ҁдղҫәլ ӳюҵҕ ԃющйլюەԺ"
+
+#: webactivity.py:410
+msgid "Cancel"
+msgstr "[юП սキ ҤШ ђЖ Հ]Ըەйҁչլ"
+
+#: webactivity.py:412 webtoolbar.py:169
+msgid "Stop"
+msgstr "[لﺳ ժҪѓэ ]کҭюu"
+
+#: webtoolbar.py:51
+msgid "Back"
+msgstr "[ғ тў یЪ Аﻼ]Ъەҁҟ"
+
+#: webtoolbar.py:58
+msgid "Forward"
+msgstr "[еհ ԸႧ цդ є أЅՅЇ]ऊօѓաەѓԃ"
+
+#: webtoolbar.py:81
+msgid "Bookmark"
+msgstr "[Я Ҫ ҵङщђ ﺳ ҎҍМչ ]Вօօкмдҕк"
+
+#: webtoolbar.py:172
+msgid "Reload"
+msgstr "[չШ ԳИւА६ҰՉАК]ЯչӀօەժ"
+
diff --git a/progresslistener.py b/progresslistener.py
new file mode 100644
index 0000000..86a6452
--- /dev/null
+++ b/progresslistener.py
@@ -0,0 +1,96 @@
+# Copyright (C) 2006, Red Hat, Inc.
+# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gobject
+import xpcom
+from xpcom.components import interfaces
+
+class ProgressListener(gobject.GObject):
+ _com_interfaces_ = interfaces.nsIWebProgressListener
+
+ __gsignals__ = {
+ 'location-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([object])),
+ 'loading-start': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([])),
+ 'loading-stop': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([])),
+ 'loading-progress': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
+ ([float]))
+ }
+
+ def __init__(self, browser):
+ gobject.GObject.__init__(self)
+
+ self._wrapped_self = xpcom.server.WrapObject(self, interfaces.nsIWebProgressListener)
+ weak_ref = xpcom.client.WeakReference(self._wrapped_self)
+
+ mask = interfaces.nsIWebProgress.NOTIFY_STATE_NETWORK | \
+ interfaces.nsIWebProgress.NOTIFY_STATE_REQUEST | \
+ interfaces.nsIWebProgress.NOTIFY_LOCATION
+ browser.web_progress.addProgressListener(self._wrapped_self, mask)
+
+ self._reset_requests_count()
+
+ def _reset_requests_count(self):
+ self.total_requests = 0
+ self.completed_requests = 0
+
+ def onLocationChange(self, webProgress, request, location):
+ self.emit('location-changed', location)
+
+ def onProgressChange(self, webProgress, request, curSelfProgress,
+ maxSelfProgress, curTotalProgress, maxTotalProgress):
+ pass
+
+ def onSecurityChange(self, webProgress, request, state):
+ pass
+
+ def onStateChange(self, webProgress, request, stateFlags, status):
+ if stateFlags & interfaces.nsIWebProgressListener.STATE_IS_REQUEST:
+ if stateFlags & interfaces.nsIWebProgressListener.STATE_START:
+ self.total_requests += 1
+ elif stateFlags & interfaces.nsIWebProgressListener.STATE_STOP:
+ self.completed_requests += 1
+
+ if stateFlags & interfaces.nsIWebProgressListener.STATE_IS_NETWORK:
+ if stateFlags & interfaces.nsIWebProgressListener.STATE_START:
+ self.emit('loading-start')
+ self._reset_requests_count()
+ elif stateFlags & interfaces.nsIWebProgressListener.STATE_STOP:
+ self.emit('loading-stop')
+
+ if self.total_requests < self.completed_requests:
+ self.emit('loading-progress', 1.0)
+ elif self.total_requests > 0:
+ self.emit('loading-progress', float(self.completed_requests) /
+ float(self.total_requests))
+ else:
+ self.emit('loading-progress', 0.0)
+
+ def onStatusChange(self, webProgress, request, status, message):
+ pass
+
+_progress_listener = None
+
+def init(browser):
+ global _progress_listener
+ _progress_listener = ProgressListener(browser)
+
+def get_instance():
+ global _progress_listener
+ return _progress_listener
diff --git a/promptservice.py b/promptservice.py
new file mode 100644
index 0000000..ca7cf37
--- /dev/null
+++ b/promptservice.py
@@ -0,0 +1,68 @@
+# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import logging
+
+import xpcom
+from xpcom import components
+from xpcom.components import interfaces
+from xpcom.server.factory import Factory
+
+class PromptService:
+ _com_interfaces_ = interfaces.nsIPromptService
+
+ cid = '{836a90cb-6304-44f0-97df-c29913b908b7}'
+ description = 'Sugar Prompt Service'
+
+ def __init__(self):
+ pass
+
+ def alert(self, parent, dialogTitle, text):
+ logging.debug('nsIPromptService.alert()')
+
+ def alertCheck(self, parent, dialogTitle, text, checkMsg, checkState):
+ logging.debug('nsIPromptService.alertCheck()')
+
+ def confirm(self, parent, dialogTitle, text):
+ logging.debug('nsIPromptService.confirm()')
+
+ def confirmCheck(self, parent, dialogTitle, text, checkMsg, checkState):
+ logging.debug('nsIPromptService.confirmCheck()')
+
+ def confirmEx(self, parent, dialogTitle, text, buttonFlags, button0Title,
+ button1Title, button2Title, checkMsg, checkState):
+ logging.debug('nsIPromptService.confirmEx()')
+
+ def prompt(self, parent, dialogTitle, text, value, checkMsg, checkState):
+ logging.debug('nsIPromptService.prompt()')
+
+ def promptPassword(self, parent, dialogTitle, text, password, checkMsg,
+ checkState):
+ logging.debug('nsIPromptService.promptPassword()')
+
+ def promptUsernameAndPassword(self, parent, dialogTitle, text, username,
+ password, checkMsg, checkState):
+ logging.debug('nsIPromptService.promptUsernameAndPassword()')
+
+ def select(self, parent, dialogTitle, text, count, selectList, outSelection):
+ logging.debug('nsIPromptService.select()')
+
+
+#components.registrar.registerFactory(PromptService.cid,
+# PromptService.description,
+# '@mozilla.org/embedcomp/prompt-service;1',
+# Factory(PromptService))
+
diff --git a/sessionstore.py b/sessionstore.py
new file mode 100644
index 0000000..2b96ea0
--- /dev/null
+++ b/sessionstore.py
@@ -0,0 +1,69 @@
+# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+# Based on
+# http://lxr.mozilla.org/seamonkey/source/browser/components/sessionstore
+
+import logging
+
+from xpcom import components
+from xpcom.components import interfaces
+
+def get_session(browser):
+ session_history = browser.web_navigation.sessionHistory
+
+ if session_history.count == 0:
+ return ''
+ return _get_history(session_history)
+
+def set_session(browser, data):
+ _set_history(browser.web_navigation.sessionHistory, data)
+
+ if data:
+ browser.web_navigation.gotoIndex(len(data) - 1)
+ else:
+ browser.load_uri('about:blank')
+
+def _get_history(history):
+ logging.debug('%r' % history.count)
+ entries_dest = []
+ for i in range(0, history.count):
+ entry_orig = history.getEntryAtIndex(i, False)
+ entry_dest = {'url': entry_orig.URI.spec,
+ 'title': entry_orig.title}
+
+ entries_dest.append(entry_dest)
+
+ return entries_dest
+
+def _set_history(history, history_data):
+ history_internal = history.queryInterface(interfaces.nsISHistoryInternal);
+
+ if history_internal.count > 0:
+ history_internal.purgeHistory(history_internal.count);
+
+ for entry_dict in history_data:
+ logging.debug('entry_dict: %r' % entry_dict)
+ entry_class = components.classes["@mozilla.org/browser/session-history-entry;1"]
+ entry = entry_class.createInstance(interfaces.nsISHEntry)
+
+ io_service_class = components.classes["@mozilla.org/network/io-service;1"]
+ io_service = io_service_class.getService(interfaces.nsIIOService)
+ entry.setURI(io_service.newURI(entry_dict['url'], None, None));
+ entry.setTitle(entry_dict['title']);
+
+ history_internal.addEntry(entry, True)
+
diff --git a/webactivity.py b/webactivity.py
new file mode 100644
index 0000000..2d2a602
--- /dev/null
+++ b/webactivity.py
@@ -0,0 +1,460 @@
+# Copyright (C) 2006, Red Hat, Inc.
+#
+# 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
+gobject.threads_init()
+
+import gtk
+import sha
+import base64
+import time
+import shutil
+
+from sugar.activity import activity
+from sugar.graphics import style
+import telepathy
+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.icon import Icon
+from sugar import mime
+
+PROFILE_VERSION = 1
+
+_profile_version = 0
+_profile_path = os.path.join(activity.get_activity_root(), 'data/gecko')
+_version_file = os.path.join(_profile_path, 'version')
+
+if os.path.exists(_version_file):
+ f = open(_version_file)
+ _profile_version = int(f.read())
+ f.close()
+
+if _profile_version < PROFILE_VERSION:
+ if not os.path.exists(_profile_path):
+ os.mkdir(_profile_path)
+
+ shutil.copy('cert8.db', _profile_path)
+ os.chmod(os.path.join(_profile_path, 'cert8.db'), 0660)
+
+ f = open(_version_file, 'w')
+ f.write(str(PROFILE_VERSION))
+ f.close()
+
+import hulahop
+hulahop.startup(_profile_path)
+
+from browser import Browser
+from webtoolbar import WebToolbar
+from viewtoolbar import ViewToolbar
+import downloadmanager
+import sessionhistory
+import progresslistener
+
+_LIBRARY_PATH = '/usr/share/library-common/index.html'
+
+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_BROWSE = 1
+
+_logger = logging.getLogger('web-activity')
+
+class WebActivity(activity.Activity):
+ def __init__(self, handle):
+ activity.Activity.__init__(self, handle)
+
+ _logger.debug('Starting the web activity')
+
+ self._browser = Browser()
+
+ temp_path = os.path.join(self.get_activity_root(), 'instance')
+ downloadmanager.init(self._browser, self, temp_path)
+ sessionhistory.init(self._browser)
+ progresslistener.init(self._browser)
+
+ toolbox = activity.ActivityToolbox(self)
+ activity_toolbar = toolbox.get_activity_toolbar()
+
+ self.toolbar = WebToolbar(self._browser)
+ toolbox.add_toolbar(_('Browse'), self.toolbar)
+ self.toolbar.show()
+
+ self._tray = HTray()
+ self.set_tray(self._tray, gtk.POS_BOTTOM)
+ self._tray.show()
+
+ self.viewtoolbar = ViewToolbar(self)
+ toolbox.add_toolbar(_('View'), self.viewtoolbar)
+ self.viewtoolbar.show()
+
+ self.set_toolbox(toolbox)
+ toolbox.show()
+
+ self.set_canvas(self._browser)
+ self._browser.show()
+
+ self.session_history = sessionhistory.get_instance()
+ self.session_history.connect('session-link-changed', self._session_history_changed_cb)
+ self.toolbar.connect('add-link', self._link_add_button_cb)
+
+ self._browser.connect("notify::title", self._title_changed_cb)
+
+ self.model = Model()
+ self.model.connect('add_link', self._add_link_model_cb)
+
+ self.current = _('blank')
+ self.webtitle = _('blank')
+ self.connect('key-press-event', self._key_press_cb)
+
+ self.toolbox.set_current_toolbar(_TOOLBAR_BROWSE)
+
+ if handle.uri:
+ self._browser.load_uri(handle.uri)
+ elif not self._jobject.file_path:
+ # TODO: we need this hack until we extend the activity API for
+ # opening URIs and default docs.
+ self._load_homepage()
+
+ self.messenger = None
+ self.connect('shared', self._shared_cb)
+
+ # Get the Presence Service
+ self.pservice = presenceservice.get_instance()
+ try:
+ name, path = self.pservice.get_preferred_connection()
+ self.tp_conn_name = name
+ self.tp_conn_path = path
+ self.conn = telepathy.client.Connection(name, path)
+ except TypeError:
+ _logger.debug('Offline')
+ self.initiating = None
+
+ if self._shared_activity is not None:
+ _logger.debug('shared: %s' %self._shared_activity.props.joined)
+
+ 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].OfferDBusTube(
+ 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].AcceptDBusTube(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)
+
+
+ def _load_homepage(self):
+ if os.path.isfile(_LIBRARY_PATH):
+ self._browser.load_uri('file://' + _LIBRARY_PATH)
+ else:
+ self._browser.load_uri('about:blank')
+
+ def _session_history_changed_cb(self, session_history, link):
+ _logger.debug('NewPage: %s.' %link)
+ self.current = link
+
+ def _title_changed_cb(self, embed, pspec):
+ if embed.props.title is not '':
+ _logger.debug('Title changed=%s' % embed.props.title)
+ self.webtitle = embed.props.title
+
+ def _get_data_from_file_path(self, file_path):
+ fd = open(file_path, 'r')
+ try:
+ data = fd.read()
+ finally:
+ fd.close()
+ return data
+
+ def read_file(self, file_path):
+ if self.metadata['mime_type'] == 'text/plain':
+ data = self._get_data_from_file_path(file_path)
+ self.model.deserialize(data)
+
+ for link in self.model.data['shared_links']:
+ _logger.debug('read: url=%s title=%s d=%s' % (link['url'],
+ link['title'],
+ link['color']))
+ self._add_link_totray(link['url'],
+ base64.b64decode(link['thumb']),
+ link['color'], link['title'],
+ link['owner'], -1, link['hash'])
+ self._browser.set_session(self.model.data['history'])
+ elif self.metadata['mime_type'] == 'text/uri-list':
+ data = self._get_data_from_file_path(file_path)
+ uris = mime.split_uri_list(data)
+ if len(uris) == 1:
+ self._browser.load_uri(uris[0])
+ else:
+ _logger.error('Open uri-list: Does not support'
+ 'list of multiple uris by now.')
+ else:
+ self._browser.load_uri(file_path)
+
+ def write_file(self, file_path):
+ if not self.metadata['mime_type']:
+ self.metadata['mime_type'] = 'text/plain'
+
+ if self.metadata['mime_type'] == 'text/plain':
+ if not self._jobject.metadata['title_set_by_user'] == '1':
+ if self._browser.props.title:
+ self.metadata['title'] = self._browser.props.title
+
+ self.model.data['history'] = self._browser.get_session()
+
+ f = open(file_path, 'w')
+ try:
+ f.write(self.model.serialize())
+ finally:
+ f.close()
+
+ def _link_add_button_cb(self, button):
+ _logger.debug('button: Add link: %s.' % self.current)
+ 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":
+ _logger.debug('keyboard: Add link: %s.' % self.current)
+ self._add_link()
+ return True
+ elif gtk.gdk.keyval_name(event.keyval) == "u":
+ _logger.debug('keyboard: Show source of the current page')
+ self._show_source()
+ return True
+ elif gtk.gdk.keyval_name(event.keyval) == "minus":
+ _logger.debug('keyboard: Zoom out')
+ self._browser.zoom_out()
+ return True
+ elif gtk.gdk.keyval_name(event.keyval) == "plus" \
+ or gtk.gdk.keyval_name(event.keyval) == "equal" :
+ _logger.debug('keyboard: Zoom in')
+ self._browser.zoom_in()
+ return True
+ elif gtk.gdk.keyval_name(event.keyval) == "XF86Start":
+ _logger.debug('keyboard: Show source of the current page SHOW_KEY')
+ self._show_source()
+ return True
+ return False
+
+ def _add_link(self):
+ ''' take screenshot and add link info to the model '''
+ for link in self.model.data['shared_links']:
+ if link['hash'] == sha.new(self.current).hexdigest():
+ _logger.debug('_add_link: link exist already a=%s b=%s' %(
+ link['hash'], sha.new(self.current).hexdigest()))
+ return
+ buffer = self._get_screenshot()
+ timestamp = time.time()
+ self.model.add_link(self.current, self.webtitle, buffer,
+ profile.get_nick_name(),
+ profile.get_color().to_string(), timestamp)
+
+ if self.messenger is not None:
+ self.messenger._add_link(self.current, self.webtitle,
+ profile.get_color().to_string(),
+ profile.get_nick_name(),
+ base64.b64encode(buffer), timestamp)
+
+ def _add_link_model_cb(self, model, index):
+ ''' receive index of new link from the model '''
+ link = self.model.data['shared_links'][index]
+ self._add_link_totray(link['url'], base64.b64decode(link['thumb']),
+ link['color'], link['title'],
+ link['owner'], index, link['hash'])
+
+ def _add_link_totray(self, url, buffer, color, title, owner, index, hash):
+ ''' add a link to the tray '''
+ item = LinkButton(url, buffer, color, title, owner, index, hash)
+ item.connect('clicked', self._link_clicked_cb, url)
+ item.connect('remove_link', self._link_removed_cb)
+ self._tray.add_item(item, index) # use index to add to the tray
+ item.show()
+ if self._tray.props.visible is False:
+ self._tray.show()
+ self.viewtoolbar.traybutton.props.sensitive = True
+
+ def _link_removed_cb(self, button, hash):
+ ''' remove a link from tray and delete it in the model '''
+ self.model.remove_link(hash)
+ self._tray.remove_item(button)
+ if len(self._tray.get_children()) == 0:
+ self.viewtoolbar.traybutton.props.sensitive = False
+
+ def _link_clicked_cb(self, button, url):
+ ''' an item of the link tray has been clicked '''
+ self._browser.load_uri(url)
+
+ def _show_source(self):
+ self._browser.get_source()
+
+ 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(100),
+ style.zoom(80),
+ gtk.gdk.INTERP_BILINEAR)
+
+ buffer = self.get_buffer(screenshot)
+ return buffer
+
+ def can_close(self):
+ if downloadmanager.can_quit():
+ return True
+ else:
+ alert = Alert()
+ alert.props.title = _('Download in progress')
+ alert.props.msg = _('Stopping now will cancel your download')
+ cancel_icon = Icon(icon_name='dialog-cancel')
+ alert.add_button(gtk.RESPONSE_CANCEL, _('Cancel'), cancel_icon)
+ stop_icon = Icon(icon_name='dialog-ok')
+ alert.add_button(gtk.RESPONSE_OK, _('Stop'), stop_icon)
+ stop_icon.show()
+ self.add_alert(alert)
+ alert.connect('response', self.__inprogress_response_cb)
+ alert.show()
+ self.present()
+
+ def __inprogress_response_cb(self, alert, response_id):
+ self.remove_alert(alert)
+ if response_id is gtk.RESPONSE_CANCEL:
+ logging.debug('Keep on')
+ elif response_id == gtk.RESPONSE_OK:
+ logging.debug('Stop downloads and quit')
+ downloadmanager.remove_all_downloads()
+ self.close(force=True)
+
diff --git a/webtoolbar.py b/webtoolbar.py
new file mode 100644
index 0000000..7a8bec5
--- /dev/null
+++ b/webtoolbar.py
@@ -0,0 +1,217 @@
+# Copyright (C) 2006, Red Hat, Inc.
+# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import logging
+from gettext import gettext as _
+
+import gobject
+import gtk
+from xpcom.components import interfaces
+from xpcom import components
+
+from sugar.graphics.toolbutton import ToolButton
+from sugar.graphics.menuitem import MenuItem
+from sugar._sugarext import AddressEntry
+import pango
+
+import sessionhistory
+import progresslistener
+import filepicker
+
+_MAX_HISTORY_ENTRIES = 15
+
+class WebToolbar(gtk.Toolbar):
+ __gtype_name__ = 'WebToolbar'
+
+ __gsignals__ = {
+ 'add-link': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ([]))
+ }
+
+ def __init__(self, browser):
+ gtk.Toolbar.__init__(self)
+
+ self._browser = browser
+
+ self._back = ToolButton('go-previous-paired')
+ self._back.set_tooltip(_('Back'))
+ self._back.props.sensitive = False
+ self._back.connect('clicked', self._go_back_cb)
+ self.insert(self._back, -1)
+ self._back.show()
+
+ self._forward = ToolButton('go-next-paired')
+ self._forward.set_tooltip(_('Forward'))
+ self._forward.props.sensitive = False
+ self._forward.connect('clicked', self._go_forward_cb)
+ self.insert(self._forward, -1)
+ self._forward.show()
+
+ self._stop_and_reload = ToolButton('media-playback-stop')
+ self._stop_and_reload.connect('clicked', self._stop_and_reload_cb)
+ self.insert(self._stop_and_reload, -1)
+ self._stop_and_reload.show()
+
+ self._entry = AddressEntry()
+ self._entry.connect('activate', self._entry_activate_cb)
+
+ entry_item = gtk.ToolItem()
+ entry_item.set_expand(True)
+ entry_item.add(self._entry)
+ self._entry.show()
+
+ self.insert(entry_item, -1)
+ entry_item.show()
+
+ self._link_add = ToolButton('emblem-favorite')
+ self._link_add.set_tooltip(_('Bookmark'))
+ self._link_add.connect('clicked', self._link_add_clicked_cb)
+ self.insert(self._link_add, -1)
+ self._link_add.show()
+
+ progress_listener = progresslistener.get_instance()
+ progress_listener.connect('location-changed', self._location_changed_cb)
+ progress_listener.connect('loading-start', self._loading_start_cb)
+ progress_listener.connect('loading-stop', self._loading_stop_cb)
+ progress_listener.connect('loading-progress', self._loading_progress_cb)
+
+ session_history = sessionhistory.get_instance()
+ session_history.connect('session-history-changed', self._session_history_changed_cb)
+
+ self._browser.connect("notify::title", self._title_changed_cb)
+
+ 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)
+
+ def _location_changed_cb(self, progress_listener, uri):
+ cls = components.classes['@mozilla.org/intl/texttosuburi;1']
+ texttosuburi = cls.getService(interfaces.nsITextToSubURI)
+ ui_uri = texttosuburi.unEscapeURIForUI(uri.originCharset, uri.spec)
+
+ self._set_address(ui_uri)
+ self._update_navigation_buttons()
+ filepicker.cleanup_temp_files()
+
+ def _loading_start_cb(self, progress_listener):
+ self._set_title(None)
+ self._set_loading(True)
+ self._update_navigation_buttons()
+
+ def _loading_stop_cb(self, progress_listener):
+ self._set_loading(False)
+ self._update_navigation_buttons()
+
+ def _loading_progress_cb(self, progress_listener, progress):
+ self._set_progress(progress)
+
+ def _set_progress(self, progress):
+ self._entry.props.progress = progress
+
+ def _set_address(self, address):
+ self._entry.props.address = address
+
+ def _set_title(self, title):
+ self._entry.props.title = title
+
+ def _show_stop_icon(self):
+ self._stop_and_reload.set_icon('media-playback-stop')
+
+ def _show_reload_icon(self):
+ self._stop_and_reload.set_icon('view-refresh')
+
+ def _update_navigation_buttons(self):
+ can_go_back = self._browser.web_navigation.canGoBack
+ self._back.props.sensitive = can_go_back
+
+ can_go_forward = self._browser.web_navigation.canGoForward
+ self._forward.props.sensitive = can_go_forward
+
+ def _entry_activate_cb(self, entry):
+ self._browser.load_uri(entry.props.text)
+ self._browser.grab_focus()
+
+ def _go_back_cb(self, button):
+ self._browser.web_navigation.goBack()
+
+ def _go_forward_cb(self, button):
+ self._browser.web_navigation.goForward()
+
+ def _title_changed_cb(self, embed, spec):
+ self._set_title(embed.props.title)
+
+ def _stop_and_reload_cb(self, button):
+ if self._loading:
+ self._browser.web_navigation.stop(interfaces.nsIWebNavigation.STOP_ALL)
+ else:
+ flags = interfaces.nsIWebNavigation.LOAD_FLAGS_NONE
+ self._browser.web_navigation.reload(flags)
+
+ def _set_loading(self, loading):
+ self._loading = loading
+
+ if self._loading:
+ self._show_stop_icon()
+ self._stop_and_reload.set_tooltip(_('Stop'))
+ else:
+ self._show_reload_icon()
+ self._stop_and_reload.set_tooltip(_('Reload'))
+
+ def _reload_session_history(self, current_page_index=None):
+ if current_page_index is None:
+ current_page_index = session_history.index
+
+ for palette in (self._back.get_palette(), self._forward.get_palette()):
+ for menu_item in palette.menu.get_children():
+ palette.menu.remove(menu_item)
+
+ session_history = self._browser.web_navigation.sessionHistory
+ if current_page_index > _MAX_HISTORY_ENTRIES:
+ bottom = current_page_index - _MAX_HISTORY_ENTRIES
+ else:
+ bottom = 0
+ if (session_history.count - current_page_index) > \
+ _MAX_HISTORY_ENTRIES:
+ top = current_page_index + _MAX_HISTORY_ENTRIES + 1
+ else:
+ top = session_history.count
+
+ for i in range(bottom, top):
+ if i == current_page_index:
+ continue
+
+ entry = session_history.getEntryAtIndex(i, False)
+ menu_item = MenuItem(entry.title, text_maxlen=60)
+ menu_item.connect('activate', self._history_item_activated_cb, i)
+
+ if i < current_page_index:
+ palette = self._back.get_palette()
+ palette.menu.prepend(menu_item)
+ elif i > current_page_index:
+ palette = self._forward.get_palette()
+ palette.menu.append(menu_item)
+
+ menu_item.show()
+
+ def _history_item_activated_cb(self, menu_item, index):
+ self._browser.web_navigation.gotoIndex(index)
+
+ def _link_add_clicked_cb(self, button):
+ self.emit('add-link')
+