qutebrowser/qutebrowser/widgets/browser.py
2014-02-23 18:14:40 +01:00

977 lines
31 KiB
Python

# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser 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 3 of the License, or
# (at your option) any later version.
#
# qutebrowser 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 qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""The main browser widget.
Defines BrowserTab (our own QWebView subclass) and TabbedBrowser (a TabWidget
containing BrowserTabs).
"""
import logging
import functools
import sip
from PyQt5.QtWidgets import QApplication, QShortcut, QSizePolicy
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QEvent, QObject
from PyQt5.QtGui import QClipboard
from PyQt5.QtPrintSupport import QPrintPreviewDialog
from PyQt5.QtNetwork import QNetworkReply
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
import qutebrowser.utils.url as urlutils
import qutebrowser.config.config as config
from qutebrowser.widgets.tabbar import TabWidget
from qutebrowser.network.networkmanager import NetworkManager
from qutebrowser.utils.signals import SignalCache, dbg_signal
from qutebrowser.utils.misc import read_file
from qutebrowser.utils.usertypes import NeighborList
class TabbedBrowser(TabWidget):
"""A TabWidget with QWebViews inside.
Provides methods to manage tabs, convenience methods to interact with the
current tab (cur_*) and filters signals to re-emit them when they occured
in the currently visible tab.
For all tab-specific signals (cur_*) emitted by a tab, this happens:
- the signal gets added to a signal_cache of the tab, so it can be
emitted again if the current tab changes.
- the signal gets filtered with _filter_signals and self.cur_* gets
emitted if the signal occured in the current tab.
Attributes:
_url_stack: Stack of URLs of closed tabs.
_space: Space QShortcut to avoid garbage collection
_tabs: A list of open tabs.
cur: A CurCommandDispatcher instance to dispatch commands to the
current tab.
Signals:
cur_progress: Progress of the current tab changed (loadProgress).
cur_load_started: Current tab started loading (loadStarted)
cur_load_finished: Current tab finished loading (loadFinished)
cur_statusbar_message: Current tab got a statusbar message
(statusBarMessage)
cur_temp_message: Current tab needs to show a temporary message.
cur_url_changed: Current URL changed (urlChanged)
cur_link_hovered: Link hovered in current tab (linkHovered)
cur_scroll_perc_changed: Scroll percentage of current tab changed.
arg 1: x-position in %.
arg 2: y-position in %.
keypress: A key was pressed.
arg: The QKeyEvent leading to the keypress.
shutdown_complete: The shuttdown is completed.
quit: The last tab was closed, quit application.
resized: Emitted when the browser window has resized, so the completion
widget can adjust its size to it.
arg: The new size.
"""
cur_progress = pyqtSignal(int)
cur_load_started = pyqtSignal()
cur_load_finished = pyqtSignal(bool)
cur_temp_message = pyqtSignal(str)
cur_statusbar_message = pyqtSignal(str)
cur_url_changed = pyqtSignal('QUrl')
cur_link_hovered = pyqtSignal(str, str, str)
cur_scroll_perc_changed = pyqtSignal(int, int)
set_cmd_text = pyqtSignal(str)
keypress = pyqtSignal('QKeyEvent')
shutdown_complete = pyqtSignal()
quit = pyqtSignal()
resized = pyqtSignal('QRect')
def __init__(self, parent=None):
super().__init__(parent)
self.currentChanged.connect(lambda idx:
self.widget(idx).signal_cache.replay())
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._tabs = []
self._url_stack = []
self._space = QShortcut(self)
self._space.setKey(Qt.Key_Space)
self._space.setContext(Qt.WidgetWithChildrenShortcut)
self._space.activated.connect(lambda: self.cur.scroll_page(0, 1))
self.cur = CurCommandDispatcher(self)
self.cur.temp_message.connect(self.cur_temp_message)
def _cb_tab_shutdown(self, tab):
"""Called after a tab has been shut down completely.
Args:
tab: The tab object which has been shut down.
Emit:
shutdown_complete: When the tab shutdown is done completely.
"""
try:
self._tabs.remove(tab)
except ValueError:
logging.exception("tab {} could not be removed".format(tab))
logging.debug("Tabs after removing: {}".format(self._tabs))
if not self._tabs: # all tabs shut down
logging.debug("Tab shutdown complete.")
self.shutdown_complete.emit()
def cntwidget(self, count=None):
"""Return a widget based on a count/idx.
Args:
count: The tab index, or None.
Return:
The current widget if count is None.
The widget with the given tab ID if count is given.
None if no widget was found.
"""
if count is None:
return self.currentWidget()
elif 1 <= count <= self.count():
return self.widget(count - 1)
else:
return None
def _titleChanged_handler(self, text):
"""Set the title of a tab.
Slot for the titleChanged signal of any tab.
Args:
text: The text to set.
"""
logging.debug('title changed to "{}"'.format(text))
if text:
self.setTabText(self.indexOf(self.sender()), text)
else:
logging.debug('ignoring title change')
def _filter_factory(self, signal):
"""Factory for partial _filter_signals functions.
Args:
signal: The pyqtSignal to filter.
Return:
A partial functon calling _filter_signals with a signal.
"""
return functools.partial(self._filter_signals, signal)
def _filter_signals(self, signal, *args):
"""Filter signals and trigger TabbedBrowser signals if needed.
Triggers signal if the original signal was sent from the _current_ tab
and not from any other one.
The original signal does not matter, since we get the new signal and
all args.
The current value of the signal is also stored in tab.signal_cache so
it can be emitted later when the tab changes to the current tab.
Args:
signal: The signal to emit if the sender was the current widget.
*args: The args to pass to the signal.
Emit:
The target signal if the sender was the current widget.
"""
# FIXME BUG the signal cache ordering seems to be weird sometimes.
# How to reproduce:
# - Open tab
# - While loading, open another tab
# - Switch back to #1 when loading finished
# - It seems loadingStarted is before loadingFinished
sender = self.sender()
log_signal = not signal.signal.startswith('2cur_progress')
if log_signal:
logging.debug('signal {} (tab {})'.format(dbg_signal(signal, args),
self.indexOf(sender)))
if not isinstance(sender, BrowserTab):
# FIXME why does this happen?
logging.warn('Got signal {} by {} which is no tab!'.format(
dbg_signal(signal, args), sender))
return
sender.signal_cache.add(signal, args)
if self.currentWidget() == sender:
if log_signal:
logging.debug(' emitting')
return signal.emit(*args)
else:
if log_signal:
logging.debug(' ignoring')
def shutdown(self):
"""Try to shut down all tabs cleanly.
Emit:
shutdown_complete if the shutdown completed successfully.
"""
try:
self.currentChanged.disconnect()
except TypeError:
pass
tabcount = self.count()
if tabcount == 0:
logging.debug("No tabs -> shutdown complete")
self.shutdown_complete.emit()
return
for tabidx in range(tabcount):
logging.debug("Shutting down tab {}/{}".format(tabidx, tabcount))
tab = self.widget(tabidx)
tab.shutdown(callback=functools.partial(self._cb_tab_shutdown,
tab))
def tabclose(self, count=None):
"""Close the current/[count]th tab.
Command handler for :close.
Args:
count: The tab index to close, or None
Emit:
quit: If last tab was closed and last_close in config is set to
quit.
"""
idx = self.currentIndex() if count is None else count - 1
tab = self.cntwidget(count)
if tab is None:
return
last_close = config.config.get('tabbar', 'last_close')
if self.count() > 1:
# FIXME maybe we actually should store the webview objects here
self._url_stack.append(tab.url())
self.removeTab(idx)
tab.shutdown(callback=functools.partial(self._cb_tab_shutdown,
tab))
elif last_close == 'quit':
self.quit.emit()
elif last_close == 'blank':
tab.openurl('about:blank')
def tabopen(self, url):
"""Open a new tab with a given url.
Also connect all the signals we need to _filter_signals.
Args:
url: The URL to open.
"""
logging.debug("Opening {}".format(url))
url = urlutils.qurl(url)
tab = BrowserTab(self)
self._tabs.append(tab)
self.addTab(tab, urlutils.urlstring(url))
self.setCurrentWidget(tab)
tab.linkHovered.connect(self._filter_factory(self.cur_link_hovered))
tab.loadProgress.connect(self._filter_factory(self.cur_progress))
tab.loadFinished.connect(self._filter_factory(self.cur_load_finished))
tab.loadStarted.connect(lambda: # pylint: disable=unnecessary-lambda
self.sender().signal_cache.clear())
tab.loadStarted.connect(self._filter_factory(self.cur_load_started))
tab.statusBarMessage.connect(
self._filter_factory(self.cur_statusbar_message))
tab.scroll_pos_changed.connect(
self._filter_factory(self.cur_scroll_perc_changed))
tab.temp_message.connect(self._filter_factory(self.cur_temp_message))
tab.urlChanged.connect(self._filter_factory(self.cur_url_changed))
tab.titleChanged.connect(self._titleChanged_handler)
# FIXME sometimes this doesn't load
tab.show()
tab.open_tab.connect(self.tabopen)
tab.openurl(url)
def tabopencur(self):
"""Set the statusbar to :tabopen and the current URL.
Emit:
set_cmd_text prefilled with :tabopen $URL
"""
url = urlutils.urlstring(self.currentWidget().url())
self.set_cmd_text.emit(':tabopen ' + url)
def opencur(self):
"""Set the statusbar to :open and the current URL.
Emit:
set_cmd_text prefilled with :open $URL
"""
url = urlutils.urlstring(self.currentWidget().url())
self.set_cmd_text.emit(':open ' + url)
def undo_close(self):
"""Undo closing a tab.
Command handler for :undo.
"""
if self._url_stack:
self.tabopen(self._url_stack.pop())
def switch_prev(self, count=1):
"""Switch to the ([count]th) previous tab.
Command handler for :tabprev.
Args:
count: How many tabs to switch back.
"""
idx = self.currentIndex()
if idx - count >= 0:
self.setCurrentIndex(idx - count)
else:
# FIXME
pass
def switch_next(self, count=1):
"""Switch to the ([count]th) next tab.
Command handler for :tabnext.
Args:
count: How many tabs to switch forward.
"""
idx = self.currentIndex()
if idx + count < self.count():
self.setCurrentIndex(idx + count)
else:
# FIXME
pass
def paste(self, sel=False):
"""Open a page from the clipboard.
Command handler for :paste.
Args:
sel: True to use primary selection, False to use clipboard
"""
# FIXME what happens for invalid URLs?
clip = QApplication.clipboard()
mode = QClipboard.Selection if sel else QClipboard.Clipboard
url = clip.text(mode)
logging.debug("Clipboard contained: '{}'".format(url))
self.cur.openurl(url)
def tabpaste(self, sel=False):
"""Open a page from the clipboard in a new tab.
Command handler for :paste.
Args:
sel: True to use primary selection, False to use clipboard
"""
# FIXME what happens for invalid URLs?
clip = QApplication.clipboard()
mode = QClipboard.Selection if sel else QClipboard.Clipboard
url = clip.text(mode)
logging.debug("Clipboard contained: '{}'".format(url))
self.tabopen(url)
def keyPressEvent(self, e):
"""Extend TabWidget (QWidget)'s keyPressEvent to emit a signal.
Args:
e: The QKeyPressEvent
Emit:
keypress: Always emitted.
"""
self.keypress.emit(e)
super().keyPressEvent(e)
def resizeEvent(self, e):
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
Args:
e: The QResizeEvent
Emit:
resize: Always emitted.
"""
super().resizeEvent(e)
self.resized.emit(self.geometry())
class CurCommandDispatcher(QObject):
"""Command dispatcher for TabbedBrowser.
Contains all commands which are related to the current tab.
Attributes:
tabs: The TabbedBrowser object.
Signals:
temp_message: Connected to TabbedBrowser signal.
"""
# FIXME maybe subclassing would be more clean?
temp_message = pyqtSignal(str)
def __init__(self, parent):
"""Constructor.
Uses setattr to get some methods from parent.
Args:
parent: The TabbedBrowser for this dispatcher.
"""
super().__init__(parent)
self.tabs = parent
def _scroll_percent(self, perc=None, count=None, orientation=None):
"""Inner logic for scroll_percent_(x|y).
Args:
perc: How many percent to scroll, or None
count: How many percent to scroll, or None
orientation: Qt.Horizontal or Qt.Vertical
"""
if perc is None and count is None:
perc = 100
elif perc is None:
perc = int(count)
else:
perc = float(perc)
frame = self.tabs.currentWidget().page_.mainFrame()
m = frame.scrollBarMaximum(orientation)
if m == 0:
return
frame.setScrollBarValue(orientation, int(m * perc / 100))
def openurl(self, url, count=None):
"""Open an url in the current/[count]th tab.
Command handler for :open.
Args:
url: The URL to open.
count: The tab index to open the URL in, or None.
"""
tab = self.tabs.cntwidget(count)
if tab is None:
if count is None:
# We want to open an URL in the current tab, but none exists
# yet.
self.tabs.tabopen(url)
else:
# Explicit count with a tab that doesn't exist.
return
else:
tab.openurl(url)
def reloadpage(self, count=None):
"""Reload the current/[count]th tab.
Command handler for :reload.
Args:
count: The tab index to reload, or None.
"""
tab = self.tabs.cntwidget(count)
if tab is not None:
tab.reload()
def stop(self, count=None):
"""Stop loading in the current/[count]th tab.
Command handler for :stop.
Args:
count: The tab index to stop, or None.
"""
tab = self.tabs.cntwidget(count)
if tab is not None:
tab.stop()
def printpage(self, count=None):
"""Print the current/[count]th tab.
Command handler for :print.
Args:
count: The tab index to print, or None.
"""
# FIXME that does not what I expect
tab = self.tabs.cntwidget(count)
if tab is not None:
preview = QPrintPreviewDialog(self)
preview.paintRequested.connect(tab.print)
preview.exec_()
def back(self, count=1):
"""Go back in the history of the current tab.
Command handler for :back.
Args:
count: How many pages to go back.
"""
# FIXME display warning if beginning of history
for i in range(count): # pylint: disable=unused-variable
self.tabs.currentWidget().back()
def forward(self, count=1):
"""Go forward in the history of the current tab.
Command handler for :forward.
Args:
count: How many pages to go forward.
"""
# FIXME display warning if end of history
for i in range(count): # pylint: disable=unused-variable
self.tabs.currentWidget().forward()
@pyqtSlot(str, int)
def search(self, text, flags):
"""Search for text in the current page.
Args:
text: The text to search for.
flags: The QWebPage::FindFlags.
"""
self.tabs.currentWidget().findText(text, flags)
def scroll(self, dx, dy, count=1):
"""Scroll the current tab by count * dx/dy.
Command handler for :scroll.
Args:
dx: How much to scroll in x-direction.
dy: How much to scroll in x-direction.
count: multiplier
"""
dx = int(count) * float(dx)
dy = int(count) * float(dy)
self.tabs.currentWidget().page_.mainFrame().scroll(dx, dy)
def scroll_percent_x(self, perc=None, count=None):
"""Scroll the current tab to a specific percent of the page.
Command handler for :scroll_perc_x.
Args:
perc: Percentage to scroll.
count: Percentage to scroll.
"""
self._scroll_percent(perc, count, Qt.Horizontal)
def scroll_percent_y(self, perc=None, count=None):
"""Scroll the current tab to a specific percent of the page.
Command handler for :scroll_perc_y
Args:
perc: Percentage to scroll.
count: Percentage to scroll.
"""
self._scroll_percent(perc, count, Qt.Vertical)
def scroll_page(self, mx, my, count=1):
"""Scroll the frame page-wise.
Args:
mx: How many pages to scroll to the right.
my: How many pages to scroll down.
count: multiplier
"""
# FIXME this might not work with HTML frames
page = self.tabs.currentWidget().page_
size = page.viewportSize()
page.mainFrame().scroll(int(count) * float(mx) * size.width(),
int(count) * float(my) * size.height())
def yank(self, sel=False):
"""Yank the current url to the clipboard or primary selection.
Command handler for :yank.
Args:
sel: True to use primary selection, False to use clipboard
Emit:
temp_message to display a temporary message.
"""
clip = QApplication.clipboard()
url = urlutils.urlstring(self.tabs.currentWidget().url())
mode = QClipboard.Selection if sel else QClipboard.Clipboard
clip.setText(url, mode)
self.temp_message.emit('URL yanked to {}'.format(
'primary selection' if sel else 'clipboard'))
def yank_title(self, sel=False):
"""Yank the current title to the clipboard or primary selection.
Command handler for :yanktitle.
Args:
sel: True to use primary selection, False to use clipboard
Emit:
temp_message to display a temporary message.
"""
clip = QApplication.clipboard()
title = self.tabs.tabText(self.tabs.currentIndex())
mode = QClipboard.Selection if sel else QClipboard.Clipboard
clip.setText(title, mode)
self.temp_message.emit('Title yanked to {}'.format(
'primary selection' if sel else 'clipboard'))
def zoom_in(self, count=1):
"""Zoom in in the current tab.
Args:
count: How many steps to take.
"""
tab = self.tabs.currentWidget()
tab.zoom(count)
def zoom_out(self, count=1):
"""Zoom out in the current tab.
Args:
count: How many steps to take.
"""
tab = self.tabs.currentWidget()
tab.zoom(-count)
class BrowserTab(QWebView):
"""One browser tab in TabbedBrowser.
Our own subclass of a QWebView with some added bells and whistles.
Attributes:
page_: The QWebPage behind the view
signal_cache: The signal cache associated with the view.
_zoom: A NeighborList with the zoom levels.
_scroll_pos: The old scroll position.
_shutdown_callback: Callback to be called after shutdown.
_open_new_tab: Whether to open a new tab for the next action.
_shutdown_callback: The callback to call after shutting down.
_destroyed: Dict of all items to be destroyed on shtudown.
Signals:
scroll_pos_changed: Scroll percentage of current tab changed.
arg 1: x-position in %.
arg 2: y-position in %.
open_tab: A new tab should be opened.
arg: The address to open
linkHovered: QWebPages linkHovered signal exposed.
temp_message: Show a temporary message in the statusbar.
arg: Message to be shown.
"""
scroll_pos_changed = pyqtSignal(int, int)
open_tab = pyqtSignal('QUrl')
linkHovered = pyqtSignal(str, str, str)
temp_message = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self._scroll_pos = (-1, -1)
self._shutdown_callback = None
self._open_new_tab = False
self._destroyed = {}
self._zoom = NeighborList(
config.config.get('general', 'zoomlevels').split(','),
default=config.config.get('general', 'defaultzoom'),
mode=NeighborList.BLOCK)
self.page_ = BrowserPage(self)
self.setPage(self.page_)
self.signal_cache = SignalCache(uncached=['linkHovered'])
self.page_.setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
self.page_.linkHovered.connect(self.linkHovered)
self.linkClicked.connect(self.on_link_clicked)
# FIXME find some way to hide scrollbars without setScrollBarPolicy
def openurl(self, url):
"""Open an URL in the browser.
Args:
url: The URL to load, as string or QUrl.
Return:
Return status of self.load
Emit:
titleChanged and urlChanged
"""
u = urlutils.fuzzy_url(url)
logging.debug('New title: {}'.format(urlutils.urlstring(u)))
self.titleChanged.emit(urlutils.urlstring(u))
self.urlChanged.emit(urlutils.qurl(u))
return self.load(u)
def zoom(self, offset):
"""Increase/Decrease the zoom level.
Args:
offset: The offset in the zoom level list.
Emit:
temp_message: Emitted with new zoom level.
"""
level = self._zoom.getitem(offset)
self.setZoomFactor(float(level) / 100)
self.temp_message.emit("Zoom level: {}%".format(level))
@pyqtSlot(str)
def on_link_clicked(self, url):
"""Handle a link.
Called from the linkClicked signal. Checks if it should open it in a
tab (middle-click or control) or not, and does so.
Args:
url: The url to handle, as string or QUrl.
Emit:
open_tab: Emitted if window should be opened in a new tab.
"""
if self._open_new_tab:
self.open_tab.emit(url)
else:
self.cur.openurl(url)
def shutdown(self, callback=None):
"""Shut down the tab cleanly and remove it.
Inspired by [1].
[1] https://github.com/integricho/path-of-a-pyqter/tree/master/qttut08
Args:
callback: Function to call after shutting down.
"""
self._shutdown_callback = callback
try:
# Avoid loading finished signal when stopping
self.loadFinished.disconnect()
except TypeError:
logging.exception("This should never happen.")
self.stop()
self.close()
self.settings().setAttribute(QWebSettings.JavascriptEnabled, False)
self._destroyed[self.page_] = False
self.page_.destroyed.connect(functools.partial(self._on_destroyed,
self.page_))
self.page_.deleteLater()
self._destroyed[self] = False
self.destroyed.connect(functools.partial(self._on_destroyed, self))
self.deleteLater()
netman = self.page_.network_access_manager
self._destroyed[netman] = False
netman.abort_requests()
netman.destroyed.connect(functools.partial(self._on_destroyed, netman))
netman.deleteLater()
logging.debug("Tab shutdown scheduled")
def _on_destroyed(self, sender):
"""Called when a subsystem has been destroyed during shutdown.
Args:
sender: The object which called the callback.
"""
self._destroyed[sender] = True
dbgout = '\n'.join(['{}: {}'.format(k.__class__.__name__, v)
for (k, v) in self._destroyed.items()])
logging.debug("{} has been destroyed, new status:\n{}".format(
sender.__class__.__name__, dbgout))
if all(self._destroyed.values()):
if self._shutdown_callback is not None:
logging.debug("Everything destroyed, calling callback")
self._shutdown_callback()
def paintEvent(self, e):
"""Extend paintEvent to emit a signal if the scroll position changed.
This is a bit of a hack: We listen to repaint requests here, in the
hope a repaint will always be requested when scrolling, and if the
scroll position actually changed, we emit a signal.
Args:
e: The QPaintEvent.
Emit:
scroll_pos_changed; If the scroll position changed.
"""
frame = self.page_.mainFrame()
new_pos = (frame.scrollBarValue(Qt.Horizontal),
frame.scrollBarValue(Qt.Vertical))
if self._scroll_pos != new_pos:
self._scroll_pos = new_pos
logging.debug("Updating scroll position")
frame = self.page_.mainFrame()
m = (frame.scrollBarMaximum(Qt.Horizontal),
frame.scrollBarMaximum(Qt.Vertical))
perc = (round(100 * new_pos[0] / m[0]) if m[0] != 0 else 0,
round(100 * new_pos[1] / m[1]) if m[1] != 0 else 0)
self.scroll_pos_changed.emit(*perc)
# Let superclass handle the event
return super().paintEvent(e)
def event(self, e):
"""Check if a link was clicked with the middle button or Ctrl.
Extend the superclass event().
This also is a bit of a hack, but it seems it's the only possible way.
Set the _open_new_tab attribute accordingly.
Args:
e: The arrived event.
Return:
The superclass event return value.
"""
if e.type() in [QEvent.MouseButtonPress, QEvent.MouseButtonDblClick]:
self._open_new_tab = (e.button() == Qt.MidButton or
e.modifiers() & Qt.ControlModifier)
return super().event(e)
class BrowserPage(QWebPage):
"""Our own QWebPage with advanced features.
Attributes:
_extension_handlers: Mapping of QWebPage extensions to their handlers.
network_access_manager: The QNetworkAccessManager used.
"""
def __init__(self, parent=None):
super().__init__(parent)
self._extension_handlers = {
QWebPage.ErrorPageExtension: self._handle_errorpage,
}
self.network_access_manager = NetworkManager(self)
self.setNetworkAccessManager(self.network_access_manager)
def _handle_errorpage(self, opt, out):
"""Display an error page if needed.
Loosly based on Helpviewer/HelpBrowserWV.py from eric5
(line 260 @ 5d937eb378dd)
Args:
opt: The QWebPage.ErrorPageExtensionOption instance.
out: The QWebPage.ErrorPageExtensionReturn instance to write return
values to.
Return:
False if no error page should be displayed, True otherwise.
"""
info = sip.cast(opt, QWebPage.ErrorPageExtensionOption)
errpage = sip.cast(out, QWebPage.ErrorPageExtensionReturn)
errpage.baseUrl = info.url
if (info.domain == QWebPage.QtNetwork and
info.error == QNetworkReply.OperationCanceledError):
return False
urlstr = urlutils.urlstring(info.url)
title = "Error loading page: {}".format(urlstr)
errpage.content = read_file('html/error.html').format(
title=title, url=urlstr, error=info.errorString, icon='')
return True
def supportsExtension(self, ext):
"""Override QWebPage::supportsExtension to provide error pages.
Args:
ext: The extension to check for.
Return:
True if the extension can be handled, False otherwise.
"""
return ext in self._extension_handlers
def extension(self, ext, opt, out):
"""Override QWebPage::extension to provide error pages.
Args:
ext: The extension.
opt: Extension options instance.
out: Extension output instance.
Return:
Handler return value.
"""
try:
handler = self._extension_handlers[ext]
except KeyError:
return super().extension(ext, opt, out)
return handler(opt, out)