| 1 |
# Copyright (C) 2006, Red Hat, Inc. |
| 2 |
# Copyright (C) 2007, One Laptop Per Child |
| 3 |
# Copyright (C) 2009, Tomeu Vizoso, Simon Schampijer |
| 4 |
# |
| 5 |
# This program is free software; you can redistribute it and/or modify |
| 6 |
# it under the terms of the GNU General Public License as published by |
| 7 |
# the Free Software Foundation; either version 2 of the License, or |
| 8 |
# (at your option) any later version. |
| 9 |
# |
| 10 |
# This program is distributed in the hope that it will be useful, |
| 11 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 |
# GNU General Public License for more details. |
| 14 |
# |
| 15 |
# You should have received a copy of the GNU General Public License |
| 16 |
# along with this program; if not, write to the Free Software |
| 17 |
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| 18 |
|
| 19 |
import os |
| 20 |
import time |
| 21 |
import re |
| 22 |
from gettext import gettext as _ |
| 23 |
|
| 24 |
from gi.repository import GObject |
| 25 |
from gi.repository import Gtk |
| 26 |
from gi.repository import Gdk |
| 27 |
from gi.repository import Pango |
| 28 |
from gi.repository import WebKit |
| 29 |
from gi.repository import Soup |
| 30 |
from gi.repository import GConf |
| 31 |
|
| 32 |
from sugar3 import env |
| 33 |
from sugar3.activity import activity |
| 34 |
from sugar3.graphics import style |
| 35 |
from sugar3.graphics.icon import Icon |
| 36 |
|
| 37 |
from widgets import BrowserNotebook |
| 38 |
from palettes import ContentInvoker |
| 39 |
from filepicker import FilePicker |
| 40 |
import globalhistory |
| 41 |
import downloadmanager |
| 42 |
from pdfviewer import PDFTabPage |
| 43 |
|
| 44 |
ZOOM_ORIGINAL = 1.0 |
| 45 |
_ZOOM_AMOUNT = 0.1 |
| 46 |
LIBRARY_PATH = '/usr/share/library-common/index.html' |
| 47 |
|
| 48 |
_WEB_SCHEMES = ['http', 'https', 'ftp', 'file', 'javascript', 'data', |
| 49 |
'about', 'gopher', 'mailto'] |
| 50 |
|
| 51 |
_NON_SEARCH_REGEX = re.compile(''' |
| 52 |
(^localhost(\\.[^\s]+)?(:\\d+)?(/.*)?$| |
| 53 |
^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]$| |
| 54 |
^::[0-9a-f:]*$| # IPv6 literals |
| 55 |
^[0-9a-f:]+:[0-9a-f:]*$| # IPv6 literals |
| 56 |
^[^\\.\s]+\\.[^\\.\s]+.*$| # foo.bar... |
| 57 |
^https?://[^/\\.\s]+.*$| |
| 58 |
^about:.*$| |
| 59 |
^data:.*$| |
| 60 |
^file:.*$) |
| 61 |
''', re.VERBOSE) |
| 62 |
|
| 63 |
DEFAULT_ERROR_PAGE = os.path.join(activity.get_bundle_path(), |
| 64 |
'data/error_page.tmpl') |
| 65 |
|
| 66 |
HOME_PAGE_GCONF_KEY = '/desktop/sugar/browser/home_page' |
| 67 |
|
| 68 |
|
| 69 |
class CommandListener(object): |
| 70 |
def __init__(self, window): |
| 71 |
self._window = window |
| 72 |
|
| 73 |
def handleEvent(self, event): |
| 74 |
if not event.isTrusted: |
| 75 |
return |
| 76 |
|
| 77 |
uri = event.originalTarget.ownerDocument.documentURI |
| 78 |
if not uri.startswith('about:neterror?e=nssBadCert'): |
| 79 |
return |
| 80 |
|
| 81 |
cls = components.classes['@sugarlabs.org/add-cert-exception;1'] |
| 82 |
cert_exception = cls.createInstance(interfaces.hulahopAddCertException) |
| 83 |
cert_exception.showDialog(self._window) |
| 84 |
|
| 85 |
|
| 86 |
class TabbedView(BrowserNotebook): |
| 87 |
__gtype_name__ = 'TabbedView' |
| 88 |
|
| 89 |
__gsignals__ = { |
| 90 |
'focus-url-entry': (GObject.SignalFlags.RUN_FIRST, |
| 91 |
None, |
| 92 |
([])), |
| 93 |
} |
| 94 |
|
| 95 |
def __init__(self): |
| 96 |
BrowserNotebook.__init__(self) |
| 97 |
|
| 98 |
self.props.show_border = False |
| 99 |
self.props.scrollable = True |
| 100 |
|
| 101 |
# Used to connect and disconnect functions when 'switch-page' |
| 102 |
self._browser = None |
| 103 |
self._load_status_changed_hid = None |
| 104 |
|
| 105 |
self.connect('size-allocate', self.__size_allocate_cb) |
| 106 |
self.connect('page-added', self.__page_added_cb) |
| 107 |
self.connect('page-removed', self.__page_removed_cb) |
| 108 |
|
| 109 |
self.connect_after('switch-page', self.__switch_page_cb) |
| 110 |
|
| 111 |
self.add_tab() |
| 112 |
self._update_closing_buttons() |
| 113 |
self._update_tab_sizes() |
| 114 |
|
| 115 |
def __switch_page_cb(self, tabbed_view, page, page_num): |
| 116 |
if tabbed_view.get_n_pages(): |
| 117 |
self._connect_to_browser(tabbed_view.props.current_browser) |
| 118 |
|
| 119 |
def _connect_to_browser(self, browser): |
| 120 |
if self._browser is not None: |
| 121 |
self._browser.disconnect(self._load_status_changed_hid) |
| 122 |
|
| 123 |
self._browser = browser |
| 124 |
self._load_status_changed_hid = self._browser.connect( |
| 125 |
'notify::load-status', self.__load_status_changed_cb) |
| 126 |
|
| 127 |
def normalize_or_autosearch_url(self, url): |
| 128 |
"""Normalize the url input or return a url for search. |
| 129 |
|
| 130 |
We use SoupURI as an indication of whether the value given in url |
| 131 |
is not something we want to search; we only do that, though, if |
| 132 |
the address has a web scheme, because SoupURI will consider any |
| 133 |
string: as a valid scheme, and we will end up prepending http:// |
| 134 |
to it. |
| 135 |
|
| 136 |
This code is borrowed from Epiphany. |
| 137 |
|
| 138 |
url -- input string that can be normalized to an url or serve |
| 139 |
as search |
| 140 |
|
| 141 |
Return: a string containing a valid url |
| 142 |
|
| 143 |
""" |
| 144 |
def has_web_scheme(address): |
| 145 |
if address == '': |
| 146 |
return False |
| 147 |
|
| 148 |
scheme, sep, after = address.partition(':') |
| 149 |
if sep == '': |
| 150 |
return False |
| 151 |
|
| 152 |
return scheme in _WEB_SCHEMES |
| 153 |
|
| 154 |
soup_uri = None |
| 155 |
effective_url = None |
| 156 |
|
| 157 |
if has_web_scheme(url): |
| 158 |
try: |
| 159 |
soup_uri = Soup.URI.new(url) |
| 160 |
except TypeError: |
| 161 |
pass |
| 162 |
|
| 163 |
if soup_uri is None and not _NON_SEARCH_REGEX.match(url): |
| 164 |
# Get the user's LANG to use as default language of |
| 165 |
# the results |
| 166 |
locale = os.environ.get('LANG', '') |
| 167 |
language_location = locale.split('.', 1)[0].lower() |
| 168 |
language = language_location.split('_')[0] |
| 169 |
# If the string doesn't look like an URI, let's search it: |
| 170 |
url_search = 'http://www.google.com/search?' \ |
| 171 |
'q=%(query)s&ie=UTF-8&oe=UTF-8&hl=%(language)s' |
| 172 |
query_param = Soup.form_encode_hash({'q': url}) |
| 173 |
# [2:] here is getting rid of 'q=': |
| 174 |
effective_url = url_search % {'query': query_param[2:], |
| 175 |
'language': language} |
| 176 |
else: |
| 177 |
if has_web_scheme(url): |
| 178 |
effective_url = url |
| 179 |
else: |
| 180 |
effective_url = 'http://' + url |
| 181 |
|
| 182 |
return effective_url |
| 183 |
|
| 184 |
def __size_allocate_cb(self, widget, allocation): |
| 185 |
self._update_tab_sizes() |
| 186 |
|
| 187 |
def __page_added_cb(self, notebook, child, pagenum): |
| 188 |
self._update_closing_buttons() |
| 189 |
self._update_tab_sizes() |
| 190 |
|
| 191 |
def __page_removed_cb(self, notebook, child, pagenum): |
| 192 |
if self.get_n_pages(): |
| 193 |
self._update_closing_buttons() |
| 194 |
self._update_tab_sizes() |
| 195 |
|
| 196 |
def __new_tab_cb(self, browser, url): |
| 197 |
new_browser = self.add_tab(next_to_current=True) |
| 198 |
new_browser.load_uri(url) |
| 199 |
new_browser.grab_focus() |
| 200 |
|
| 201 |
def __create_web_view_cb(self, web_view, frame): |
| 202 |
new_web_view = Browser() |
| 203 |
new_web_view.connect('web-view-ready', self.__web_view_ready_cb) |
| 204 |
return new_web_view |
| 205 |
|
| 206 |
def __web_view_ready_cb(self, web_view): |
| 207 |
""" |
| 208 |
Handle new window requested and open it in a new tab. |
| 209 |
|
| 210 |
This callback is called when the WebKit.WebView request for a |
| 211 |
new window to open (for example a call to the Javascript |
| 212 |
function 'window.open()' or target="_blank") |
| 213 |
|
| 214 |
web_view -- the new browser there the url of the |
| 215 |
window.open() call will be loaded. |
| 216 |
|
| 217 |
This object is created in the signal callback |
| 218 |
'create-web-view'. |
| 219 |
""" |
| 220 |
|
| 221 |
web_view.connect('new-tab', self.__new_tab_cb) |
| 222 |
web_view.connect('open-pdf', self.__open_pdf_in_new_tab_cb) |
| 223 |
web_view.connect('create-web-view', self.__create_web_view_cb) |
| 224 |
web_view.grab_focus() |
| 225 |
|
| 226 |
self._insert_tab_next(web_view) |
| 227 |
|
| 228 |
def __open_pdf_in_new_tab_cb(self, browser, url): |
| 229 |
tab_page = PDFTabPage() |
| 230 |
tab_page.browser.connect('new-tab', self.__new_tab_cb) |
| 231 |
tab_page.browser.connect('tab-close', self.__tab_close_cb) |
| 232 |
|
| 233 |
label = TabLabel(tab_page.browser) |
| 234 |
label.connect('tab-close', self.__tab_close_cb, tab_page) |
| 235 |
|
| 236 |
next_index = self.get_current_page() + 1 |
| 237 |
self.insert_page(tab_page, label, next_index) |
| 238 |
tab_page.show() |
| 239 |
label.show() |
| 240 |
self.set_current_page(next_index) |
| 241 |
tab_page.setup(url) |
| 242 |
|
| 243 |
def __load_status_changed_cb(self, widget, param): |
| 244 |
if self.get_window() is None: |
| 245 |
return |
| 246 |
|
| 247 |
status = widget.get_load_status() |
| 248 |
if status in (WebKit.LoadStatus.PROVISIONAL, |
| 249 |
WebKit.LoadStatus.COMMITTED, |
| 250 |
WebKit.LoadStatus.FIRST_VISUALLY_NON_EMPTY_LAYOUT): |
| 251 |
self.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.WATCH)) |
| 252 |
elif status in (WebKit.LoadStatus.FAILED, |
| 253 |
WebKit.LoadStatus.FINISHED): |
| 254 |
self.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.LEFT_PTR)) |
| 255 |
|
| 256 |
def add_tab(self, next_to_current=False): |
| 257 |
browser = Browser() |
| 258 |
browser.connect('new-tab', self.__new_tab_cb) |
| 259 |
browser.connect('open-pdf', self.__open_pdf_in_new_tab_cb) |
| 260 |
browser.connect('web-view-ready', self.__web_view_ready_cb) |
| 261 |
browser.connect('create-web-view', self.__create_web_view_cb) |
| 262 |
|
| 263 |
if next_to_current: |
| 264 |
self._insert_tab_next(browser) |
| 265 |
else: |
| 266 |
self._append_tab(browser) |
| 267 |
self.emit('focus-url-entry') |
| 268 |
return browser |
| 269 |
|
| 270 |
def _insert_tab_next(self, browser): |
| 271 |
tab_page = TabPage(browser) |
| 272 |
label = TabLabel(browser) |
| 273 |
label.connect('tab-close', self.__tab_close_cb, tab_page) |
| 274 |
|
| 275 |
next_index = self.get_current_page() + 1 |
| 276 |
self.insert_page(tab_page, label, next_index) |
| 277 |
tab_page.show() |
| 278 |
self.set_current_page(next_index) |
| 279 |
|
| 280 |
def _append_tab(self, browser): |
| 281 |
tab_page = TabPage(browser) |
| 282 |
label = TabLabel(browser) |
| 283 |
label.connect('tab-close', self.__tab_close_cb, tab_page) |
| 284 |
|
| 285 |
self.append_page(tab_page, label) |
| 286 |
tab_page.show() |
| 287 |
self.set_current_page(-1) |
| 288 |
|
| 289 |
def on_add_tab(self, gobject): |
| 290 |
self.add_tab() |
| 291 |
|
| 292 |
def close_tab(self, tab_page=None): |
| 293 |
if self.get_n_pages() == 1: |
| 294 |
return |
| 295 |
|
| 296 |
if tab_page is None: |
| 297 |
tab_page = self.get_nth_page(self.get_current_page()) |
| 298 |
|
| 299 |
if isinstance(tab_page, PDFTabPage): |
| 300 |
if tab_page.props.browser.props.load_status < \ |
| 301 |
WebKit.LoadStatus.FINISHED: |
| 302 |
tab_page.cancel_download() |
| 303 |
|
| 304 |
self.remove_page(self.page_num(tab_page)) |
| 305 |
|
| 306 |
current_page = self.get_nth_page(self.get_current_page()) |
| 307 |
current_page.props.browser.grab_focus() |
| 308 |
|
| 309 |
def __tab_close_cb(self, label, tab_page): |
| 310 |
self.close_tab(tab_page) |
| 311 |
|
| 312 |
def _update_tab_sizes(self): |
| 313 |
"""Update tab widths based in the amount of tabs.""" |
| 314 |
|
| 315 |
n_pages = self.get_n_pages() |
| 316 |
canvas_size = self.get_allocation() |
| 317 |
allowed_size = canvas_size.width |
| 318 |
if n_pages == 1: |
| 319 |
# use half of the whole space |
| 320 |
tab_expand = False |
| 321 |
tab_new_size = int(allowed_size / 2) |
| 322 |
elif n_pages <= 8: # ensure eight tabs |
| 323 |
tab_expand = True # use all the space available by tabs |
| 324 |
tab_new_size = -1 |
| 325 |
else: |
| 326 |
# scroll the tab toolbar if there are more than 8 tabs |
| 327 |
tab_expand = False |
| 328 |
tab_new_size = (allowed_size / 8) |
| 329 |
|
| 330 |
for page_idx in range(n_pages): |
| 331 |
page = self.get_nth_page(page_idx) |
| 332 |
label = self.get_tab_label(page) |
| 333 |
self.child_set_property(page, 'tab-expand', tab_expand) |
| 334 |
label.update_size(tab_new_size) |
| 335 |
|
| 336 |
def _update_closing_buttons(self): |
| 337 |
"""Prevent closing the last tab.""" |
| 338 |
first_page = self.get_nth_page(0) |
| 339 |
first_label = self.get_tab_label(first_page) |
| 340 |
if self.get_n_pages() == 1: |
| 341 |
first_label.hide_close_button() |
| 342 |
else: |
| 343 |
first_label.show_close_button() |
| 344 |
|
| 345 |
def load_homepage(self, ignore_gconf=False): |
| 346 |
browser = self.current_browser |
| 347 |
uri_homepage = None |
| 348 |
if not ignore_gconf: |
| 349 |
client = GConf.Client.get_default() |
| 350 |
uri_homepage = client.get_string(HOME_PAGE_GCONF_KEY) |
| 351 |
if uri_homepage is not None: |
| 352 |
browser.load_uri(uri_homepage) |
| 353 |
elif os.path.isfile(LIBRARY_PATH): |
| 354 |
browser.load_uri('file://' + LIBRARY_PATH) |
| 355 |
else: |
| 356 |
default_page = os.path.join(activity.get_bundle_path(), |
| 357 |
"data/index.html") |
| 358 |
browser.load_uri('file://' + default_page) |
| 359 |
browser.grab_focus() |
| 360 |
|
| 361 |
def set_homepage(self): |
| 362 |
uri = self.current_browser.get_uri() |
| 363 |
client = GConf.Client.get_default() |
| 364 |
client.set_string(HOME_PAGE_GCONF_KEY, uri) |
| 365 |
|
| 366 |
def reset_homepage(self): |
| 367 |
client = GConf.Client.get_default() |
| 368 |
client.unset(HOME_PAGE_GCONF_KEY) |
| 369 |
|
| 370 |
def _get_current_browser(self): |
| 371 |
if self.get_n_pages(): |
| 372 |
return self.get_nth_page(self.get_current_page()).browser |
| 373 |
else: |
| 374 |
return None |
| 375 |
|
| 376 |
current_browser = GObject.property(type=object, |
| 377 |
getter=_get_current_browser) |
| 378 |
|
| 379 |
def get_history(self): |
| 380 |
tab_histories = [] |
| 381 |
for index in xrange(0, self.get_n_pages()): |
| 382 |
tab_page = self.get_nth_page(index) |
| 383 |
tab_histories.append(tab_page.browser.get_history()) |
| 384 |
return tab_histories |
| 385 |
|
| 386 |
def set_history(self, tab_histories): |
| 387 |
if tab_histories and isinstance(tab_histories[0], dict): |
| 388 |
# Old format, no tabs |
| 389 |
tab_histories = [tab_histories] |
| 390 |
|
| 391 |
while self.get_n_pages(): |
| 392 |
self.remove_page(self.get_n_pages() - 1) |
| 393 |
|
| 394 |
def is_pdf_history(tab_history): |
| 395 |
return (len(tab_history) == 1 and |
| 396 |
tab_history[0]['url'].lower().endswith('pdf')) |
| 397 |
|
| 398 |
for tab_history in tab_histories: |
| 399 |
if is_pdf_history(tab_history): |
| 400 |
url = tab_history[0]['url'] |
| 401 |
tab_page = PDFTabPage() |
| 402 |
tab_page.browser.connect('new-tab', self.__new_tab_cb) |
| 403 |
tab_page.browser.connect('tab-close', self.__tab_close_cb) |
| 404 |
|
| 405 |
label = TabLabel(tab_page.browser) |
| 406 |
label.connect('tab-close', self.__tab_close_cb, tab_page) |
| 407 |
|
| 408 |
self.append_page(tab_page, label) |
| 409 |
tab_page.show() |
| 410 |
label.show() |
| 411 |
tab_page.setup(url, title=tab_history[0]['title']) |
| 412 |
|
| 413 |
else: |
| 414 |
browser = Browser() |
| 415 |
browser.connect('new-tab', self.__new_tab_cb) |
| 416 |
browser.connect('open-pdf', self.__open_pdf_in_new_tab_cb) |
| 417 |
browser.connect('web-view-ready', self.__web_view_ready_cb) |
| 418 |
browser.connect('create-web-view', self.__create_web_view_cb) |
| 419 |
self._append_tab(browser) |
| 420 |
browser.set_history(tab_history) |
| 421 |
|
| 422 |
def is_current_page_pdf(self): |
| 423 |
index = self.get_current_page() |
| 424 |
current_page = self.get_nth_page(index) |
| 425 |
return isinstance(current_page, PDFTabPage) |
| 426 |
|
| 427 |
|
| 428 |
Gtk.rc_parse_string(''' |
| 429 |
style "browse-tab-close" { |
| 430 |
xthickness = 0 |
| 431 |
ythickness = 0 |
| 432 |
} |
| 433 |
widget "*browse-tab-close" style "browse-tab-close"''') |
| 434 |
|
| 435 |
|
| 436 |
class TabPage(Gtk.ScrolledWindow): |
| 437 |
__gtype_name__ = 'BrowseTabPage' |
| 438 |
|
| 439 |
def __init__(self, browser): |
| 440 |
GObject.GObject.__init__(self) |
| 441 |
|
| 442 |
self._browser = browser |
| 443 |
|
| 444 |
self.add(browser) |
| 445 |
browser.show() |
| 446 |
|
| 447 |
def _get_browser(self): |
| 448 |
return self._browser |
| 449 |
|
| 450 |
browser = GObject.property(type=object, |
| 451 |
getter=_get_browser) |
| 452 |
|
| 453 |
|
| 454 |
class TabLabel(Gtk.HBox): |
| 455 |
__gtype_name__ = 'BrowseTabLabel' |
| 456 |
|
| 457 |
__gsignals__ = { |
| 458 |
'tab-close': (GObject.SignalFlags.RUN_FIRST, |
| 459 |
None, |
| 460 |
([])), |
| 461 |
} |
| 462 |
|
| 463 |
def __init__(self, browser): |
| 464 |
GObject.GObject.__init__(self) |
| 465 |
|
| 466 |
browser.connect('notify::title', self.__title_changed_cb) |
| 467 |
browser.connect('notify::load-status', self.__load_status_changed_cb) |
| 468 |
|
| 469 |
self._title = _('Untitled') |
| 470 |
self._label = Gtk.Label(label=self._title) |
| 471 |
self._label.set_ellipsize(Pango.EllipsizeMode.END) |
| 472 |
self._label.set_alignment(0, 0.5) |
| 473 |
self.pack_start(self._label, True, True, 0) |
| 474 |
self._label.show() |
| 475 |
|
| 476 |
close_tab_icon = Icon(icon_name='browse-close-tab') |
| 477 |
button = Gtk.Button() |
| 478 |
button.props.relief = Gtk.ReliefStyle.NONE |
| 479 |
button.props.focus_on_click = False |
| 480 |
icon_box = Gtk.HBox() |
| 481 |
icon_box.pack_start(close_tab_icon, True, False, 0) |
| 482 |
button.add(icon_box) |
| 483 |
button.connect('clicked', self.__button_clicked_cb) |
| 484 |
button.set_name('browse-tab-close') |
| 485 |
self.pack_start(button, False, True, 0) |
| 486 |
close_tab_icon.show() |
| 487 |
icon_box.show() |
| 488 |
button.show() |
| 489 |
self._close_button = button |
| 490 |
|
| 491 |
def update_size(self, size): |
| 492 |
self.set_size_request(size, -1) |
| 493 |
|
| 494 |
def hide_close_button(self): |
| 495 |
self._close_button.hide() |
| 496 |
|
| 497 |
def show_close_button(self): |
| 498 |
self._close_button.show() |
| 499 |
|
| 500 |
def __button_clicked_cb(self, button): |
| 501 |
self.emit('tab-close') |
| 502 |
|
| 503 |
def __title_changed_cb(self, widget, param): |
| 504 |
title = widget.props.title |
| 505 |
if not title: |
| 506 |
title = os.path.basename(widget.props.uri) |
| 507 |
|
| 508 |
self._label.set_text(title) |
| 509 |
self._title = title |
| 510 |
|
| 511 |
def __load_status_changed_cb(self, widget, param): |
| 512 |
status = widget.get_load_status() |
| 513 |
|
| 514 |
if status == WebKit.LoadStatus.FAILED: |
| 515 |
self._label.set_text(self._title) |
| 516 |
elif WebKit.LoadStatus.PROVISIONAL <= status \ |
| 517 |
< WebKit.LoadStatus.FINISHED: |
| 518 |
self._label.set_text(_('Loading...')) |
| 519 |
elif status == WebKit.LoadStatus.FINISHED: |
| 520 |
if widget.props.title == None: |
| 521 |
self._label.set_text(_('Untitled')) |
| 522 |
self._title = _('Untitled') |
| 523 |
|
| 524 |
|
| 525 |
class Browser(WebKit.WebView): |
| 526 |
__gtype_name__ = 'Browser' |
| 527 |
|
| 528 |
__gsignals__ = { |
| 529 |
'new-tab': (GObject.SignalFlags.RUN_FIRST, |
| 530 |
None, |
| 531 |
([str])), |
| 532 |
'open-pdf': (GObject.SignalFlags.RUN_FIRST, |
| 533 |
None, |
| 534 |
([str])), |
| 535 |
'security-status-changed': (GObject.SignalFlags.RUN_FIRST, |
| 536 |
None, |
| 537 |
([])), |
| 538 |
} |
| 539 |
|
| 540 |
CURRENT_SUGAR_VERSION = '0.100' |
| 541 |
|
| 542 |
SECURITY_STATUS_SECURE = 1 |
| 543 |
SECURITY_STATUS_INSECURE = 2 |
| 544 |
|
| 545 |
def __init__(self): |
| 546 |
WebKit.WebView.__init__(self) |
| 547 |
|
| 548 |
web_settings = self.get_settings() |
| 549 |
|
| 550 |
# Add SugarLabs user agent: |
| 551 |
identifier = ' SugarLabs/' + self.CURRENT_SUGAR_VERSION |
| 552 |
web_settings.props.user_agent += identifier |
| 553 |
|
| 554 |
# Change font size based in the GtkSettings font size. The |
| 555 |
# gtk-font-name property is a string with format '[font name] |
| 556 |
# [font size]' like 'Sans Serif 10'. |
| 557 |
gtk_settings = Gtk.Settings.get_default() |
| 558 |
gtk_font_name = gtk_settings.get_property('gtk-font-name') |
| 559 |
gtk_font_size = float(gtk_font_name.split()[-1]) |
| 560 |
web_settings.props.default_font_size = gtk_font_size * 1.2 |
| 561 |
web_settings.props.default_monospace_font_size = \ |
| 562 |
gtk_font_size * 1.2 - 2 |
| 563 |
|
| 564 |
self.set_settings(web_settings) |
| 565 |
|
| 566 |
# Scale text and graphics: |
| 567 |
self.set_full_content_zoom(True) |
| 568 |
|
| 569 |
# This property is used to set the title immediatly the user |
| 570 |
# presses Enter on the URL Entry |
| 571 |
self.loading_uri = None |
| 572 |
|
| 573 |
self.security_status = None |
| 574 |
|
| 575 |
# Reference to the global history and callbacks to handle it: |
| 576 |
self._global_history = globalhistory.get_global_history() |
| 577 |
self.connect('notify::load-status', self.__load_status_changed_cb) |
| 578 |
self.connect('notify::title', self.__title_changed_cb) |
| 579 |
self.connect('download-requested', self.__download_requested_cb) |
| 580 |
self.connect('mime-type-policy-decision-requested', |
| 581 |
self.__mime_type_policy_cb) |
| 582 |
self.connect('load-error', self.__load_error_cb) |
| 583 |
|
| 584 |
self._inject_media_style = False |
| 585 |
|
| 586 |
ContentInvoker(self) |
| 587 |
|
| 588 |
try: |
| 589 |
self.connect('run-file-chooser', self.__run_file_chooser) |
| 590 |
except TypeError: |
| 591 |
# Only present in WebKit1 > 1.9.3 and WebKit2 |
| 592 |
pass |
| 593 |
|
| 594 |
def get_history(self): |
| 595 |
"""Return the browsing history of this browser.""" |
| 596 |
back_forward_list = self.get_back_forward_list() |
| 597 |
items_list = self._items_history_as_list(back_forward_list) |
| 598 |
|
| 599 |
# If this is an empty tab, return an empty history: |
| 600 |
if len(items_list) == 1 and items_list[0] is None: |
| 601 |
return [] |
| 602 |
|
| 603 |
history = [] |
| 604 |
for item in items_list: |
| 605 |
history.append({'url': item.get_uri(), |
| 606 |
'title': item.get_title()}) |
| 607 |
|
| 608 |
return history |
| 609 |
|
| 610 |
def set_history(self, history): |
| 611 |
"""Restore the browsing history for this browser.""" |
| 612 |
back_forward_list = self.get_back_forward_list() |
| 613 |
back_forward_list.clear() |
| 614 |
for entry in history: |
| 615 |
uri, title = entry['url'], entry['title'] |
| 616 |
history_item = WebKit.WebHistoryItem.new_with_data(uri, title) |
| 617 |
back_forward_list.add_item(history_item) |
| 618 |
|
| 619 |
def get_history_index(self): |
| 620 |
"""Return the index of the current item in the history.""" |
| 621 |
back_forward_list = self.get_back_forward_list() |
| 622 |
history_list = self._items_history_as_list(back_forward_list) |
| 623 |
current_item = back_forward_list.get_current_item() |
| 624 |
return history_list.index(current_item) |
| 625 |
|
| 626 |
def set_history_index(self, index): |
| 627 |
"""Go to the item in the history specified by the index.""" |
| 628 |
back_forward_list = self.get_back_forward_list() |
| 629 |
current_item = index - back_forward_list.get_back_length() |
| 630 |
item = back_forward_list.get_nth_item(current_item) |
| 631 |
if item is not None: |
| 632 |
self.go_to_back_forward_item(item) |
| 633 |
|
| 634 |
def _items_history_as_list(self, history): |
| 635 |
"""Return a list with the items of a WebKit.WebBackForwardList.""" |
| 636 |
back_items = [] |
| 637 |
for n in reversed(range(1, history.get_back_length() + 1)): |
| 638 |
item = history.get_nth_item(n * -1) |
| 639 |
back_items.append(item) |
| 640 |
|
| 641 |
current_item = [history.get_current_item()] |
| 642 |
|
| 643 |
forward_items = [] |
| 644 |
for n in range(1, history.get_forward_length() + 1): |
| 645 |
item = history.get_nth_item(n) |
| 646 |
forward_items.append(item) |
| 647 |
|
| 648 |
all_items = back_items + current_item + forward_items |
| 649 |
return all_items |
| 650 |
|
| 651 |
def get_source(self, async_cb, async_err_cb): |
| 652 |
data_source = self.get_main_frame().get_data_source() |
| 653 |
data = data_source.get_data() |
| 654 |
if data_source.is_loading() or data is None: |
| 655 |
async_err_cb() |
| 656 |
temp_path = os.path.join(activity.get_activity_root(), 'instance') |
| 657 |
file_path = os.path.join(temp_path, '%i' % time.time()) |
| 658 |
|
| 659 |
file_handle = file(file_path, 'w') |
| 660 |
file_handle.write(data.str) |
| 661 |
file_handle.close() |
| 662 |
async_cb(file_path) |
| 663 |
|
| 664 |
def open_new_tab(self, url): |
| 665 |
self.emit('new-tab', url) |
| 666 |
|
| 667 |
def __run_file_chooser(self, browser, request): |
| 668 |
picker = FilePicker(self) |
| 669 |
chosen = picker.run() |
| 670 |
picker.destroy() |
| 671 |
|
| 672 |
if chosen: |
| 673 |
request.select_files([chosen]) |
| 674 |
elif hasattr(request, 'cancel'): |
| 675 |
# WebKit2 only |
| 676 |
request.cancel() |
| 677 |
return True |
| 678 |
|
| 679 |
def __load_status_changed_cb(self, widget, param): |
| 680 |
status = widget.get_load_status() |
| 681 |
if status <= WebKit.LoadStatus.COMMITTED: |
| 682 |
# Add the url to the global history or update it. |
| 683 |
uri = self.get_uri() |
| 684 |
self._global_history.add_page(uri) |
| 685 |
|
| 686 |
if status == WebKit.LoadStatus.COMMITTED: |
| 687 |
# Update the security status. |
| 688 |
response = widget.get_main_frame().get_network_response() |
| 689 |
message = response.get_message() |
| 690 |
if message: |
| 691 |
use_https, certificate, tls_errors = message.get_https_status() |
| 692 |
|
| 693 |
if use_https: |
| 694 |
if tls_errors == 0: |
| 695 |
self.security_status = self.SECURITY_STATUS_SECURE |
| 696 |
else: |
| 697 |
self.security_status = self.SECURITY_STATUS_INSECURE |
| 698 |
else: |
| 699 |
self.security_status = None |
| 700 |
self.emit('security-status-changed') |
| 701 |
|
| 702 |
def __title_changed_cb(self, widget, param): |
| 703 |
"""Update title in global history.""" |
| 704 |
uri = self.get_uri() |
| 705 |
if self.props.title is not None: |
| 706 |
title = self.props.title |
| 707 |
if not isinstance(title, unicode): |
| 708 |
title = unicode(title, 'utf-8') |
| 709 |
self._global_history.set_page_title(uri, title) |
| 710 |
|
| 711 |
def __mime_type_policy_cb(self, webview, frame, request, mimetype, |
| 712 |
policy_decision): |
| 713 |
"""Handle downloads and PDF files.""" |
| 714 |
if mimetype == 'application/pdf': |
| 715 |
self.emit('open-pdf', request.get_uri()) |
| 716 |
policy_decision.ignore() |
| 717 |
return True |
| 718 |
|
| 719 |
elif mimetype == 'audio/x-vorbis+ogg': |
| 720 |
self._inject_media_style = True |
| 721 |
|
| 722 |
elif not self.can_show_mime_type(mimetype): |
| 723 |
policy_decision.download() |
| 724 |
return True |
| 725 |
|
| 726 |
return False |
| 727 |
|
| 728 |
def __download_requested_cb(self, browser, download): |
| 729 |
downloadmanager.add_download(download, browser) |
| 730 |
return True |
| 731 |
|
| 732 |
def __load_error_cb(self, web_view, web_frame, uri, web_error): |
| 733 |
"""Show Sugar's error page""" |
| 734 |
|
| 735 |
# Don't show error page if the load was interrupted by policy |
| 736 |
# change or the request is going to be handled by a |
| 737 |
# plugin. For example, if a file was requested for download or |
| 738 |
# an .ogg file is going to be played. |
| 739 |
if web_error.code in (WebKit.PolicyError.\ |
| 740 |
FRAME_LOAD_INTERRUPTED_BY_POLICY_CHANGE, |
| 741 |
WebKit.PluginError.WILL_HANDLE_LOAD): |
| 742 |
if self._inject_media_style: |
| 743 |
css_style_file = open(os.path.join(activity.get_bundle_path(), |
| 744 |
"data/media-controls.css")) |
| 745 |
css_style = css_style_file.read().replace('\n', '') |
| 746 |
inject_style_script = \ |
| 747 |
"var style = document.createElement('style');" \ |
| 748 |
"style.innerHTML = '%s';" \ |
| 749 |
"document.body.appendChild(style);" % css_style |
| 750 |
web_view.execute_script(inject_style_script) |
| 751 |
return True |
| 752 |
|
| 753 |
data = { |
| 754 |
'page_title': _('This web page could not be loaded'), |
| 755 |
'title': _('This web page could not be loaded'), |
| 756 |
'message': _('"%s" could not be loaded. Please check for ' |
| 757 |
'typing errors, and make sure you are connected ' |
| 758 |
'to the Internet.') % uri, |
| 759 |
'btn_value': _('Try again'), |
| 760 |
'url': uri, |
| 761 |
} |
| 762 |
|
| 763 |
html = open(DEFAULT_ERROR_PAGE, 'r').read() % data |
| 764 |
web_frame.load_alternate_string(html, uri, uri) |
| 765 |
|
| 766 |
return True |
| 767 |
|
| 768 |
|
| 769 |
class PopupDialog(Gtk.Window): |
| 770 |
def __init__(self): |
| 771 |
GObject.GObject.__init__(self) |
| 772 |
|
| 773 |
self.set_type_hint(Gdk.WindowTypeHint.DIALOG) |
| 774 |
|
| 775 |
border = style.GRID_CELL_SIZE |
| 776 |
self.set_default_size(Gdk.Screen.width() - border * 2, |
| 777 |
Gdk.Screen.height() - border * 2) |
| 778 |
|
| 779 |
self.view = WebKit.WebView() |
| 780 |
self.view.connect('notify::visibility', self.__notify_visibility_cb) |
| 781 |
self.add(self.view) |
| 782 |
self.view.realize() |
| 783 |
|
| 784 |
def __notify_visibility_cb(self, web_view, pspec): |
| 785 |
if self.view.props.visibility: |
| 786 |
self.view.show() |
| 787 |
self.show() |